JavaScript 徹底入門 (ES2021 版)
本書のサポートをお願いします: 購入 または 寄付
(広告、ブロックしないでください。)

29 プロトタイプチェーンとクラス



本書では、JavaScript のオブジェクト指向プログラミング (OOP) のスタイルを 4 つのステップで紹介します。この章ではステップ 2~4 を扱い、前の章ではステップ 1 を扱っています。ステップは次のとおりです (図 9)。

  1. 単一オブジェクト (前の章): JavaScript の基本的な OOP の構成要素であるオブジェクトは、単独ではどのように機能するのでしょうか?
  2. プロトタイプチェーン (この章): 各オブジェクトには、0 個以上のプロトタイプオブジェクトのチェーンがあります。プロトタイプは JavaScript のコアとなる継承メカニズムです。
  3. クラス (この章): JavaScript のクラスはオブジェクトのファクトリーです。クラスとそのインスタンスの関係は、プロトタイプベースの継承に基づいています。
  4. サブクラス化 (この章): サブクラスとそのスーパークラスの関係も、プロトタイプベースの継承に基づいています。
Figure 9: This book introduces object-oriented programming in JavaScript in four steps.

29.1 プロトタイプチェーン

プロトタイプは、JavaScript の唯一の継承メカニズムです。各オブジェクトには、null またはオブジェクトであるプロトタイプがあります。後者の場合、オブジェクトはプロトタイプのすべてのプロパティを継承します。

オブジェクトリテラルでは、特別なプロパティ __proto__ を使用してプロトタイプを設定できます。

const proto = {
  protoProp: 'a',
};
const obj = {
  __proto__: proto,
  objProp: 'b',
};

// obj inherits .protoProp:
assert.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true);

プロトタイプオブジェクト自体がプロトタイプを持つことができるため、オブジェクトのチェーン (いわゆるプロトタイプチェーン) が作成されます。つまり、継承によって単一のオブジェクトを扱っているように見えますが、実際にはオブジェクトのチェーンを扱っています。

10 は、obj のプロトタイプチェーンがどのように見えるかを示しています。

Figure 10: obj starts a chain of objects that continues with proto and other objects.

継承されないプロパティは、自身のプロパティと呼ばれます。obj には、自身のプロパティが 1 つあります (.objProp)。

29.1.1 JavaScript の操作: すべてのプロパティと自身のプロパティ

一部の操作では、すべてのプロパティ (自身と継承されたプロパティ) を考慮します。たとえば、プロパティを取得する場合などです。

> const obj = { foo: 1 };
> typeof obj.foo // own
'number'
> typeof obj.toString // inherited
'function'

その他の操作では、自身のプロパティのみを考慮します。たとえば、Object.keys() などです。

> Object.keys(obj)
[ 'foo' ]

自身のプロパティのみを考慮する別の操作 (プロパティの設定) について、以下をお読みください。

29.1.2 落とし穴: プロトタイプチェーンの最初のメンバーのみが変更される

プロトタイプチェーンの直感的ではない側面の 1 つは、オブジェクトを介して任意のプロパティ (継承されたプロパティでさえ) を設定すると、そのオブジェクト自体のみが変更され、プロトタイプのいずれかが変更されることはないということです。

次のオブジェクト obj を検討してください。

const proto = {
  protoProp: 'a',
};
const obj = {
  __proto__: proto,
  objProp: 'b',
};

次のコードスニペットでは、継承されたプロパティ obj.protoProp (行 A) を設定します。これにより、自身のプロパティを作成することで "変更" されます。obj.protoProp を読み取ると、自身のプロパティが最初に検出され、その値が継承されたプロパティの値をオーバーライドします。

// In the beginning, obj has one own property
assert.deepEqual(Object.keys(obj), ['objProp']);

obj.protoProp = 'x'; // (A)

// We created a new own property:
assert.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);

// The inherited property itself is unchanged:
assert.equal(proto.protoProp, 'a');

// The own property overrides the inherited property:
assert.equal(obj.protoProp, 'x');

obj のプロトタイプチェーンは、図 11 に示されています。

Figure 11: The own property .protoProp of obj overrides the property inherited from proto.

29.1.3 プロトタイプを扱うためのヒント (高度)

29.1.3.1 ベストプラクティス: オブジェクトリテラル以外では __proto__ を避ける

擬似プロパティ __proto__ を避けることをお勧めします。後で見るように、すべてのオブジェクトがそれを持っているわけではありません。

ただし、オブジェクトリテラル内の __proto__ は異なります。ここでは、組み込み機能であり、常に使用できます。

プロトタイプを取得および設定するための推奨される方法は次のとおりです。

これらの機能の使用方法を以下に示します。

const proto1 = {};
const proto2 = {};

const obj = Object.create(proto1);
assert.equal(Object.getPrototypeOf(obj), proto1);

Object.setPrototypeOf(obj, proto2);
assert.equal(Object.getPrototypeOf(obj), proto2);
29.1.3.2 チェック: あるオブジェクトが別のオブジェクトのプロトタイプであるか?

これまで、「po のプロトタイプである」という場合は、常に「po直接のプロトタイプである」という意味でした。しかし、より緩く使用して、po のプロトタイプチェーン内にあるという意味で使用することもできます。より緩やかな関係は、次のようにして確認できます。

p.isPrototypeOf(o)

たとえば

const a = {};
const b = {__proto__: a};
const c = {__proto__: b};

assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);

assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);

29.1.4 プロトタイプを介したデータの共有

次のコードを検討してください。

const jane = {
  name: 'Jane',
  describe() {
    return 'Person named '+this.name;
  },
};
const tarzan = {
  name: 'Tarzan',
  describe() {
    return 'Person named '+this.name;
  },
};

assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');

非常によく似た 2 つのオブジェクトがあります。どちらにも .name.describe という名前の 2 つのプロパティがあります。さらに、メソッド .describe() は同じです。このメソッドの複製を避けるにはどうすればよいでしょうか?

オブジェクト PersonProto に移動し、そのオブジェクトを janetarzan の両方のプロトタイプにすることができます。

const PersonProto = {
  describe() {
    return 'Person named ' + this.name;
  },
};
const jane = {
  __proto__: PersonProto,
  name: 'Jane',
};
const tarzan = {
  __proto__: PersonProto,
  name: 'Tarzan',
};

プロトタイプの名前は、janetarzan の両方が人物であることを反映しています。

Figure 12: Objects jane and tarzan share method .describe(), via their common prototype PersonProto.

12 は、3 つのオブジェクトがどのように接続されているかを示しています。下部のオブジェクトには、janetarzan に固有のプロパティが含まれるようになりました。上部のオブジェクトには、それらの間で共有されるプロパティが含まれています。

メソッド呼び出し jane.describe() を行うと、this はそのメソッド呼び出しのレシーバーである jane (図の左下隅) を指します。そのため、メソッドは引き続き機能します。tarzan.describe() も同様に機能します。

assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');

29.2 クラス

プロトタイプチェーンを設定するためのコンパクトな構文であるクラスに取り組む準備が整いました。内部的には、JavaScript のクラスは型破りです。しかし、それはクラスを扱うときにめったに見られないことです。他のオブジェクト指向プログラミング言語を使用したことがある人にとっては、通常は使い慣れているはずです。

29.2.1 人物のためのクラス

以前は、人物を表す単一のオブジェクトである janetarzan を使用していました。クラス宣言を使用して、人物オブジェクトのファクトリーを実装しましょう。

class Person {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return 'Person named '+this.name;
  }
}

janetarzan は、new Person() を使用して作成できるようになりました。

const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');
assert.equal(jane.describe(), 'Person named Jane');

const tarzan = new Person('Tarzan');
assert.equal(tarzan.name, 'Tarzan');
assert.equal(tarzan.describe(), 'Person named Tarzan');

クラス Person には、2 つのメソッドがあります。

29.2.1.1 クラス式

クラス定義 (クラスを定義する方法) には、2 つの種類があります。

クラス式には、匿名と名前付きの 2 つがあります。

// Anonymous class expression
const Person = class { ··· };

// Named class expression
const Person = class MyClass { ··· };

名前付きクラス式の名前は、名前付き関数式の名前と同様に機能します。

これはクラスの最初の一瞥でした。すぐにさらに多くの機能を調べますが、最初にクラスの内部構造を学習する必要があります。

29.2.2 クラスの内部構造

クラスの内部では多くのことが起こっています。jane の図を見てみましょう (図 13)。

Figure 13: The class Person has the property .prototype that points to an object that is the prototype of all instances of Person. jane is one such instance.

クラス Person の主な目的は、右側のプロトタイプチェーン (jane の後に Person.prototype が続く) を設定することです。クラス Person 内の両方の構成要素 (.constructor.describe()) が、Person ではなく、Person.prototype のプロパティを作成したことに注目するのは興味深いことです。

この少し奇妙なアプローチの理由は、下位互換性です。クラスの前は、コンストラクター関数 (通常の関数new 演算子を介して呼び出される) がオブジェクトのファクトリーとしてよく使用されていました。クラスは、コンストラクター関数に対するほとんどが優れた構文であるため、古いコードとの互換性を維持しています。それがクラスが関数である理由です。

> typeof Person
'function'

本書では、コンストラクター (関数)クラスという用語を同義で使用します。

.__proto__.prototype を混同しやすいです。図 13 で、それらの違いが明確になることを願っています。

29.2.2.1 Person.prototype.constructor (高度)

13 には、まだ見ていない詳細が 1 つあります。Person.prototype.constructorPerson を指し返します。

> Person.prototype.constructor === Person
true

この設定も下位互換性のために存在します。ただし、追加のメリットが 2 つあります。

まず、クラスの各インスタンスはプロパティ.constructorを継承します。したがって、インスタンスが与えられた場合、それを使用して「類似の」オブジェクトを作成できます。

const jane = new Person('Jane');

const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
// (the instanceof operator is explained later)
assert.equal(cheeta instanceof Person, true);

次に、与えられたインスタンスを作成したクラスの名前を取得できます。

const tarzan = new Person('Tarzan');

assert.equal(tarzan.constructor.name, 'Person');

29.2.3 クラス定義: プロトタイププロパティ

次のクラス宣言の本体にあるすべての構造は、Foo.prototypeのプロパティを作成します。

class Foo {
  constructor(prop) {
    this.prop = prop;
  }
  protoMethod() {
    return 'protoMethod';
  }
  get protoGetter() {
    return 'protoGetter';
  }
}

順番に見ていきましょう。

次のインタラクションでは、クラスFooを使用します。

> const foo = new Foo(123);
> foo.prop
123

> foo.protoMethod()
'protoMethod'
> foo.protoGetter
'protoGetter'

29.2.4 クラス定義: 静的プロパティ

次のクラス宣言の本体にあるすべての構造は、いわゆる静的プロパティ、つまりBar自体のプロパティを作成します。

class Bar {
  static staticMethod() {
    return 'staticMethod';
  }
  static get staticGetter() {
    return 'staticGetter';
  }
}

静的メソッドと静的ゲッターは次のように使用されます。

> Bar.staticMethod()
'staticMethod'
> Bar.staticGetter
'staticGetter'

29.2.5 instanceof演算子

instanceof演算子は、値が与えられたクラスのインスタンスであるかどうかを判断します。

> new Person('Jane') instanceof Person
true
> ({}) instanceof Person
false
> ({}) instanceof Object
true
> [] instanceof Array
true

サブクラス化を見た後で、instanceof演算子について後ほど詳しく見ていきます。

29.2.6 クラスを推奨する理由

次の理由から、クラスの使用をお勧めします。

それは、クラスが完璧であることを意味するものではありません。

  演習: クラスの作成

exercises/proto-chains-classes/point_class_test.mjs

29.3 クラスのプライベートデータ

このセクションでは、オブジェクトの一部のデータを外部から隠すためのテクニックについて説明します。クラスのコンテキストでそれらについて説明しますが、オブジェクトリテラルなどを介して直接作成されたオブジェクトにも有効です。

29.3.1 プライベートデータ: 命名規則

最初のテクニックは、名前の前にアンダースコアを付けることでプロパティをプライベートにします。これはプロパティを保護するものではなく、単に外部に「このプロパティについて知る必要はない」というシグナルを送るだけです。

次のコードでは、プロパティ._counter._actionがプライベートです。

class Countdown {
  constructor(counter, action) {
    this._counter = counter;
    this._action = action;
  }
  dec() {
    this._counter--;
    if (this._counter === 0) {
      this._action();
    }
  }
}

// The two properties aren’t really private:
assert.deepEqual(
  Object.keys(new Countdown()),
  ['_counter', '_action']);

このテクニックでは、保護はまったく得られず、プライベートな名前が衝突する可能性があります。利点としては、使いやすいことが挙げられます。

29.3.2 プライベートデータ: WeakMaps

もう1つのテクニックはWeakMapsを使用することです。それがどのように正確に機能するかについては、WeakMapsに関する章で説明します。これはプレビューです。

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

// The two pseudo-properties are truly private:
assert.deepEqual(
  Object.keys(new Countdown()),
  []);

このテクニックは、外部からのアクセスに対してかなりの保護を提供し、名前の衝突も発生しません。しかし、使用するのはより複雑です。

29.3.3 プライベートデータのためのその他のテクニック

この本では、クラスのプライベートデータに関する最も重要なテクニックを説明します。また、おそらくすぐに組み込みサポートも提供されるでしょう。詳細については、ECMAScriptの提案“クラスの公開インスタンスフィールドとプライベートインスタンスフィールド”を参照してください。

いくつかの追加のテクニックは、Exploring ES6で説明されています。

29.4 サブクラス化

クラスは、既存のクラスをサブクラス化(「拡張」)することもできます。例として、次のクラスEmployeePersonをサブクラス化します。

class Person {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return `Person named ${this.name}`;
  }
  static logNames(persons) {
    for (const person of persons) {
      console.log(person.name);
    }
  }
}

class Employee extends Person {
  constructor(name, title) {
    super(name);
    this.title = title;
  }
  describe() {
    return super.describe() +
      ` (${this.title})`;
  }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
  jane.describe(),
  'Person named Jane (CTO)');

2つのコメント

  演習: サブクラス化

exercises/proto-chains-classes/color_point_class_test.mjs

29.4.1 内部のサブクラス(高度な内容)

Figure 14: These are the objects that make up class Person and its subclass, Employee. The left column is about classes. The right column is about the Employee instance jane and its prototype chain.

前のセクションのクラスPersonEmployeeは、いくつかのオブジェクトで構成されています(図 14)。これらのオブジェクトがどのように関連しているかを理解するための重要な洞察の1つは、2つのプロトタイプチェーンがあることです。

29.4.1.1 インスタンスプロトタイプチェーン(右側の列)

インスタンスプロトタイプチェーンはjaneで始まり、Employee.prototypePerson.prototypeと続きます。原則として、プロトタイプチェーンはここで終わりますが、もう1つのオブジェクト、Object.prototypeが得られます。このプロトタイプは、ほぼすべてのオブジェクトにサービスを提供するため、ここにも含まれています。

> Object.getPrototypeOf(Person.prototype) === Object.prototype
true
29.4.1.2 クラスプロトタイプチェーン(左側の列)

クラスプロトタイプチェーンでは、最初にEmployee、次にPersonが来ます。その後、チェーンはFunction.prototypeへと続きます。これは、Personが関数であり、関数にはFunction.prototypeのサービスが必要なためだけに存在します。

> Object.getPrototypeOf(Person) === Function.prototype
true

29.4.2 instanceofの詳細(高度な内容)

instanceofが実際にどのように機能するかはまだ見ていません。式が与えられた場合

x instanceof C

instanceofは、xCのインスタンス(またはCのサブクラス)であるかどうかをどのように判断しますか?これは、C.prototypexのプロトタイプチェーン内にあるかどうかを確認することによって行います。つまり、次の式は同等です。

C.prototype.isPrototypeOf(x)

図 14に戻ると、プロトタイプチェーンが次の正しい答えに導いてくれることを確認できます。

> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
true

29.4.3 組み込みオブジェクトのプロトタイプチェーン(高度な内容)

次に、サブクラス化の知識を使用して、いくつかの組み込みオブジェクトのプロトタイプチェーンを理解します。次のツール関数p()が調査に役立ちます。

const p = Object.getPrototypeOf.bind(Object);

Objectのメソッド.getPrototypeOf()を抽出し、pに割り当てました。

29.4.3.1 {}のプロトタイプチェーン

まず、プレーンオブジェクトを調べてみましょう。

> p({}) === Object.prototype
true
> p(p({})) === null
true
Figure 15: The prototype chain of an object created via an object literal starts with that object, continues with Object.prototype, and ends with null.

図 15は、このプロトタイプチェーンの図を示しています。{}が実際にObjectのインスタンスであること、つまりObject.prototypeがそのプロトタイプチェーン内にあることがわかります。

29.4.3.2 []のプロトタイプチェーン

Arrayのプロトタイプチェーンはどのようになっているでしょうか。

> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true
Figure 16: The prototype chain of an Array has these members: the Array instance, Array.prototype, Object.prototype, null.

このプロトタイプチェーン(図 16で視覚化)は、ArrayオブジェクトがArrayのインスタンスであり、Objectのサブクラスであることを示しています。

29.4.3.3 function () {}のプロトタイプチェーン

最後に、通常の関数のプロトタイプチェーンは、すべての関数がオブジェクトであることを示しています。

> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true
29.4.3.4 Objectのインスタンスではないオブジェクト

Object.prototypeがそのプロトタイプチェーンにある場合のみ、オブジェクトはObjectのインスタンスです。さまざまなリテラルを介して作成されたほとんどのオブジェクトは、Objectのインスタンスです。

> ({}) instanceof Object
true
> (() => {}) instanceof Object
true
> /abc/ug instanceof Object
true

プロトタイプを持たないオブジェクトは、Objectのインスタンスではありません。

> ({ __proto__: null }) instanceof Object
false

Object.prototypeはほとんどのプロトタイプチェーンを終わらせます。そのプロトタイプはnullであり、これはObjectのインスタンスでもないことを意味します。

> Object.prototype instanceof Object
false
29.4.3.5 疑似プロパティ.__proto__は正確にはどのように機能しますか?

疑似プロパティ.__proto__は、getterとsetterを介してクラスObjectによって実装されます。これは次のように実装できます。

class Object {
  get __proto__() {
    return Object.getPrototypeOf(this);
  }
  set __proto__(other) {
    Object.setPrototypeOf(this, other);
  }
  // ···
}

つまり、プロトタイプチェーンにObject.prototypeがないオブジェクトを作成することで、.__proto__をオフにすることができます(前のセクションを参照)。

> '__proto__' in {}
true
> '__proto__' in { __proto__: null }
false

29.4.4 ディスパッチされたメソッド呼び出しと直接メソッド呼び出し(高度な内容)

クラスでのメソッド呼び出しのしくみを調べてみましょう。以前のjaneを再訪します。

class Person {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return 'Person named '+this.name;
  }
}
const jane = new Person('Jane');

図 17は、janeのプロトタイプチェーンの図を示しています。

Figure 17: The prototype chain of jane starts with jane and continues with Person.prototype.

通常のメソッド呼び出しはディスパッチされます。つまり、メソッド呼び出しjane.describe()は2つのステップで発生します。

このメソッドを動的に探し出して呼び出す方法は、動的ディスパッチと呼ばれます。

ディスパッチせずに、直接同じメソッド呼び出しを行うことができます。

Person.prototype.describe.call(jane)

今回は、Person.prototype.describeを介してメソッドを直接指定し、プロトタイプチェーンで検索しません。また、.call()を介してthisを異なる方法で指定します。

this は常にプロトタイプチェーンの先頭を指すことに注意してください。これにより、.describe().name にアクセスできます。

29.4.4.1 メソッドの借用

Object.prototype のメソッドを扱う場合、直接メソッド呼び出しが役立ちます。たとえば、Object.prototype.hasOwnProperty(k) は、this がキーが k である非継承プロパティを持っているかどうかを確認します。

> const obj = { foo: 123 };
> obj.hasOwnProperty('foo')
true
> obj.hasOwnProperty('bar')
false

ただし、オブジェクトのプロトタイプチェーンには、Object.prototype のメソッドをオーバーライドするキー 'hasOwnProperty' を持つ別のプロパティが存在する可能性があります。その場合、ディスパッチされたメソッド呼び出しは機能しません。

> const obj = { hasOwnProperty: true };
> obj.hasOwnProperty('bar')
TypeError: obj.hasOwnProperty is not a function

回避策は、直接メソッド呼び出しを使用することです。

> Object.prototype.hasOwnProperty.call(obj, 'bar')
false
> Object.prototype.hasOwnProperty.call(obj, 'hasOwnProperty')
true

この種の直接メソッド呼び出しは、次のように省略されることがよくあります。

> ({}).hasOwnProperty.call(obj, 'bar')
false
> ({}).hasOwnProperty.call(obj, 'hasOwnProperty')
true

このパターンは非効率的に見えるかもしれませんが、ほとんどのエンジンはこのパターンを最適化するため、パフォーマンスの問題にはならないはずです。

29.4.5 ミックスインクラス(高度な内容)

JavaScript のクラスシステムは単一継承のみをサポートしています。つまり、各クラスは最大で1つのスーパークラスを持つことができます。この制限を回避する1つの方法は、ミックスインクラス(略称:ミックスイン)と呼ばれる手法を使用することです。

考え方は次のとおりです。クラス C が2つのスーパークラス S1S2 から継承したいとしましょう。これは多重継承となり、JavaScript ではサポートされていません。

回避策は、S1S2 をサブクラスのファクトリーであるミックスインに変換することです。

const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };

これら2つの関数はそれぞれ、指定されたスーパークラス Sup を拡張するクラスを返します。クラス C は次のように作成します。

class C extends S2(S1(Object)) {
  /*···*/
}

これで、Object(ほとんどのクラスが暗黙的に行う)を拡張するクラス S1 を拡張するクラス S2 を拡張するクラス C ができました。

29.4.5.1 例:ブランド管理用のミックスイン

オブジェクトのブランドを設定および取得するためのヘルパーメソッドを持つミックスイン Branded を実装します。

const Branded = (Sup) => class extends Sup {
  setBrand(brand) {
    this._brand = brand;
    return this;
  }
  getBrand() {
    return this._brand;
  }
};

このミックスインを使用して、クラス Car のブランド管理を実装します。

class Car extends Branded(Object) {
  constructor(model) {
    super();
    this._model = model;
  }
  toString() {
    return `${this.getBrand()} ${this._model}`;
  }
}

次のコードは、ミックスインが機能したことを確認します。Car には Branded のメソッド .setBrand() があります。

const modelT = new Car('Model T').setBrand('Ford');
assert.equal(modelT.toString(), 'Ford Model T');
29.4.5.2 ミックスインの利点

ミックスインは、単一継承の制約から私たちを解放します。

29.5 FAQ:オブジェクト

29.5.1 オブジェクトがプロパティの挿入順序を保持するのはなぜですか?

原則として、オブジェクトは順序付けられていません。プロパティを順序付けする主な理由は、エントリ、キー、または値をリストする操作が確定的になるようにするためです。これは、たとえばテストに役立ちます。

  クイズ

クイズアプリ を参照してください。