JavaScript/TypeScript

クラスとプロトタイプの関係

5
  • JavaScript

クラスとプロトタイプの関係 — JavaScript Tips

class は見た目こそクラス型言語に近いですが、中身はプロトタイプ継承です。コンストラクタ関数と prototype と同じモデルを、宣言的に書く構文だと捉えると extends やメソッド共有の動きが追いやすくなります。

参考: クラス · 継承とプロトタイプチェーン · constructor · extends · super


目次

  1. class は何の糖衣構文か
  2. クラスの定義とインスタンス化
  3. メソッドとプロパティ
  4. プロトタイプチェーンの基本
  5. extends で継承する
  6. 静的メソッドとプライベートフィールド
  7. クラスとオブジェクトリテラルの使い分け
  8. よくある失敗

class は何の糖衣構文か

ES2015 以前は、コンストラクタ関数と prototype プロパティで「クラス風」のコードを書いていました。

function Animal(name) {  this.name = name;}Animal.prototype.speak = function () {  return `${this.name} が鳴く`;};const dog = new Animal("ポチ");console.log(dog.speak()); // "ポチ が鳴く"

class はこれと同等のことを、より宣言的に書けます。

class Animal {  constructor(name) {    this.name = name;  }  speak() {    return `${this.name} が鳴く`;  }}const dog = new Animal("ポチ");console.log(dog.speak());

class で定義したメソッドも、インスタンスのプロトタイプに載ります。見た目はクラス、実体はプロトタイプだと考えると、継承と this の読み方がぶれにくくなります。


クラスの定義とインスタンス化

class User {  constructor(id, name) {    this.id = id;    this.name = name;  }  greet() {    return `こんにちは、${this.name} さん`;  }}const u = new User(1, "山田");console.log(u.greet()); // "こんにちは、山田 さん"console.log(u instanceof User); // true

クラスは通常の関数と違い、new なしでは呼び出せません。これはクラス構文固有の仕様で、strict モードかどうかに関係なく TypeError になります。

// User(1, "山田"); // TypeError

constructor はインスタンス生成時に 1 回 呼ばれ、初期化を担当します。戻り値を明示的にオブジェクト以外にしても、基本的には new が作ったインスタンスが返ります。


メソッドとプロパティ

インスタンスメソッド

クラス本文のメソッドはプロトタイプに定義され、すべてのインスタンスで共有されます。

class Counter {  constructor() {    this.count = 0;  }  increment() {    this.count += 1;  }}const a = new Counter();const b = new Counter();a.increment();console.log(a.count); // 1console.log(b.count); // 0 — インスタンスごとのデータ

ゲッター・セッター

class Rectangle {  constructor(width, height) {    this.width = width;    this.height = height;  }  get area() {    return this.width * this.height;  }  set area(value) {    throw new Error("area は直接代入できません");  }}const rect = new Rectangle(4, 5);console.log(rect.area); // 20

クラスフィールド(モダン構文)

class Config {  static version = "1.0.0";  debug = false;  constructor(env) {    if (env === "dev") this.debug = true;  }}

フィールドはインスタンスまたはクラス(static)に直接プロパティが作られます。


プロトタイプチェーンの基本

オブジェクトには内部の [[Prototype]]__proto__ ではなく、通常は Object.getPrototypeOf で参照)があり、プロパティが見つからないとき 上位のプロトタイプへ辿る のがプロトタイプチェーンです。

class Person {  constructor(name) {    this.name = name;  }  hello() {    return `Hello, ${this.name}`;  }}const alice = new Person("Alice");// alice に hello があるか?console.log(alice.hasOwnProperty("name"));  // true — 自身のプロパティconsole.log(alice.hasOwnProperty("hello")); // false — プロトタイプ側console.log(alice.hello()); // プロトタイプのメソッドが呼ばれる

イメージ図(概念):

alice オブジェクト  name: "Alice"  [[Prototype]] → Person.prototype                    hello: function ...                    [[Prototype]] → Object.prototype

instanceof は、コンストラクタの prototype がプロトタイプチェーン上にあるかを調べます。

console.log(alice instanceof Person);  // trueconsole.log(alice instanceof Object);  // true

extends で継承する

extends はプロトタイプチェーンをつなぎ、サブクラスがスーパークラスの振る舞いを引き継ぎます。

class Animal {  constructor(name) {    this.name = name;  }  speak() {    return `${this.name} が鳴く`;  }}class Dog extends Animal {  constructor(name, breed) {    super(name); // 親の constructor を呼ぶ(this 使用前に必須)    this.breed = breed;  }  speak() {    return `${super.speak()}(犬種: ${this.breed})`;  }}const pochi = new Dog("ポチ", "柴");console.log(pochi.speak());// "ポチ が鳴く(犬種: 柴)"

super の 2 つの使い方

  1. super(...) … 親クラスの constructor 呼び出し
  2. super.method() … 親クラスのメソッド呼び出し

サブクラスで constructor を定義した場合、this を使う前に super() を呼ぶ 必要があります。これを忘れると ReferenceError になります。


静的メソッドとプライベートフィールド

static

クラス自体に紐づくメソッドで、インスタンスからは呼べません。

class MathUtil {  static clamp(value, min, max) {    return Math.min(max, Math.max(min, value));  }}console.log(MathUtil.clamp(150, 0, 100)); // 100

ファクトリメソッドやユーティリティのまとめ置きに使います。

class User {  constructor(name) {    this.name = name;  }  static fromJSON(json) {    const data = JSON.parse(json);    return new User(data.name);  }}

プライベートフィールド #

class BankAccount {  #balance = 0;  deposit(amount) {    if (amount > 0) this.#balance += amount;  }  getBalance() {    return this.#balance;  }}const account = new BankAccount();account.deposit(1000);console.log(account.getBalance()); // 1000// account.#balance; // 構文エラー — クラス外からアクセス不可

# 付きフィールドは本当にクラス内部だけのカプセル化です(命名規則の _private より強い)。


クラスとオブジェクトリテラルの使い分け

状況 おすすめ
単発のデータ束 オブジェクトリテラル { }
複数インスタンスを同じ型で作る class
継承・メソッド共有が必要 class
設定や DTO だけ リテラル + 型(TypeScript なら interface)
// インスタンスを何度も作るなら classclass Point {  constructor(x, y) {    this.x = x;    this.y = y;  }}// 1 回きりの設定ならリテラルで十分const appConfig = {  apiUrl: "/api",  timeout: 5000,};

よくある失敗

extends したのに super() を忘れた

class Child extends Parent {  constructor(value) {    // super() なしで this を使う    this.value = value; // ReferenceError  }}

アロー関数のクラスフィールドを増やしすぎる

クラスフィールドのアロー関数では、this は各インスタンスに固定されます。一方、関数もインスタンスごとに作られるため、プロトタイプで共有する通常のメソッドよりメモリコストが増えます。通常のメソッド構文を基本にし、そのままコールバックへ渡しても this を維持したい場合などに検討します。

class Handler {  // 各インスタンスに関数がコピーされる  onClick = () => {    console.log(this);  };}

プロトタイプを直接いじって class と混同

class Foo {}Foo.prototype.extra = function () {};

動きますが、可読性と保守性のため クラス本文にメソッドを書く 方が一般的です。

instanceof が期待と違う

別フレーム(iframe)越しのオブジェクトなど、プロトタイプが一致しない環境では instanceof が false になることがあります。厳密な型判定には Symbol タグや constructor.name、TypeScript の型など別手段を検討します。

深い継承階層

継承は強力ですが、階層が深くなると挙動の追跡が難しくなります。合成(他オブジェクトをプロパティとして持つ)や、小さな関数の組み合わせも選択肢に入れてください。

class Flying {  fly() { return "飛ぶ"; }}class Swimming {  swim() { return "泳ぐ"; }}// 多重継承の代わりに委譲class Duck {  constructor() {    this.flying = new Flying();    this.swimming = new Swimming();  }}

シェア