JavaScript for impatient programmers (ES2022 edition)
本書をサポートしてください:購入する または 寄付する
(広告です。ブロックしないでください。)

29 クラス [ES6]



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

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

29.1 チートシート:クラス

スーパークラス

class Person {
  #firstName; // (A)
  constructor(firstName) {
    this.#firstName = firstName; // (B)
  }
  describe() {
    return `Person named ${this.#firstName}`;
  }
  static extractNames(persons) {
    return persons.map(person => person.#firstName);
  }
}
const tarzan = new Person('Tarzan');
assert.equal(
  tarzan.describe(),
  'Person named Tarzan'
);
assert.deepEqual(
  Person.extractNames([tarzan, new Person('Cheeta')]),
  ['Tarzan', 'Cheeta']
);

サブクラス

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

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

備考

29.2 クラスの基本

クラスは基本的に、プロトタイプチェーン(前の章で説明)を設定するためのコンパクトな構文です。内部的には、JavaScriptのクラスは型破りです。しかし、それらを使用する際にはめったに見ることはありません。通常、他のオブジェクト指向プログラミング言語を使用したことがある人にとってはなじみのあるものとなるはずです。

オブジェクトを作成するためにクラスは必要ありません。オブジェクトリテラルを使用して作成することもできます。そのため、シングルトンパターンはJavaScriptでは必要なく、クラスはそれを持つ他の多くの言語よりも使用頻度が低くなっています。

29.2.1 人物を表すクラス

これまでに、人物を表す単一オブジェクトであるjanetarzanを扱ってきました。そのようなオブジェクトのファクトリを実装するために、クラス宣言を使用してみましょう。

class Person {
  #firstName; // (A)
  constructor(firstName) {
    this.#firstName = firstName; // (B)
  }
  describe() {
    return `Person named ${this.#firstName}`;
  }
  static extractNames(persons) {
    return persons.map(person => person.#firstName);
  }
}

janetarzanは、これでnew Person()を使って作成できます。

const jane = new Person('Jane');
const tarzan = new Person('Tarzan');

クラスPersonの本体の中身を調べてみましょう。

コンストラクタでインスタンスプロパティ(パブリックフィールド)を作成することもできます。

class Container {
  constructor(value) {
    this.value = value;
  }
}
const abcContainer = new Container('abc');
assert.equal(
  abcContainer.value, 'abc'
);

インスタンスプライベートフィールドとは異なり、インスタンスプロパティはクラス本体で宣言する必要はありません。

29.2.2 クラス式

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

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

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

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

名前付きクラス式の名前は、名前付き関数式の名前と同様に機能します。クラスの本体内でのみアクセスでき、クラスに割り当てられているものとは関係なく同じままです。

29.2.3 instanceof 演算子

instanceof演算子は、値が特定のクラスのインスタンスかどうかを判断します。

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

サブクラス化について詳しく見てから、後でinstanceof演算子をさらに詳しく調べます。

29.2.4 パブリックスロット(プロパティ)とプライベートスロット

JavaScriptでは、オブジェクトは2種類の「スロット」を持つことができます。

プロパティとプライベートスロットについて知っておくべき最も重要なルールは以下のとおりです。

  プロパティとプライベートスロットに関する詳細情報

この章では、プロパティとプライベートスロットのすべての詳細(基本的なもののみ)を網羅していません。さらに詳しく調べたい場合は、以下を参照してください。

次のクラスは、2種類のスロットを示しています。各インスタンスは、1つのプライベートフィールドと1つのプロパティを持っています。

class MyClass {
  #instancePrivateField = 1;
  instanceProperty = 2;
  getInstanceValues() {
    return [
      this.#instancePrivateField,
      this.instanceProperty,
    ];
  }
}
const inst = new MyClass();
assert.deepEqual(
  inst.getInstanceValues(), [1, 2]
);

予想通り、MyClassの外側からは、プロパティしか見えません。

assert.deepEqual(
  Reflect.ownKeys(inst),
  ['instanceProperty']
);

次に、プライベートスロットの詳細の一部を見ていきます。

29.2.5 プライベートスロットの詳細 [ES2022](上級)

29.2.5.1 プライベートスロットはサブクラスではアクセスできない

プライベートスロットは、実際にはクラスの本体の内側からしかアクセスできません。サブクラスからもアクセスできません。

class SuperClass {
  #superProp = 'superProp';
}
class SubClass extends SuperClass {
  getSuperProp() {
    return this.#superProp;
  }
}
// SyntaxError: Private field '#superProp'
// must be declared in an enclosing class

extendsによるサブクラス化については、この章の後半で説明します。この制限を回避する方法については、§29.5.4「WeakMapを使用した保護された可視性とフレンド可視性のシミュレーション」で説明しています。

29.2.5.2 各プライベートスロットは一意のキー(プライベート名)を持つ

プライベートスロットは、シンボルに似た一意のキーを持っています。以前のクラスを考えてみましょう。

class MyClass {
  #instancePrivateField = 1;
  instanceProperty = 2;
  getInstanceValues() {
    return [
      this.#instancePrivateField,
      this.instanceProperty,
    ];
  }
}

内部的には、MyClassのプライベートフィールドは、おおよそ次のように処理されます。

let MyClass;
{ // Scope of the body of the class
  const instancePrivateFieldKey = Symbol();
  MyClass = class {
    // Very loose approximation of how this
    // works in the language specification
    __PrivateElements__ = new Map([
      [instancePrivateFieldKey, 1],
    ]);
    instanceProperty = 2;
    getInstanceValues() {
      return [
        this.__PrivateElements__.get(instancePrivateFieldKey),
        this.instanceProperty,
      ];
    }
  }
}

instancePrivateFieldKeyの値は、プライベート名と呼ばれます。プライベート名はJavaScriptでは直接使用できず、プライベートフィールド、プライベートメソッド、プライベートアクセサの固定識別子を通じて間接的にのみ使用できます。(getInstanceValuesのような)パブリックスロットの固定識別子は文字列キーとして解釈されるのに対し、(#instancePrivateFieldのような)プライベートスロットの固定識別子は、プライベート名を参照します(変数名が値を参照する方法と同様です)。

29.2.5.3 プライベート識別子は、異なるクラスでは異なるプライベート名を指す

プライベートスロットの識別子はキーとして使用されないため、異なるクラスで同じ識別子を使用しても、異なるスロットが生成されます(A行とC行)。

class Color {
  #name; // (A)
  constructor(name) {
    this.#name = name; // (B)
  }
  static getName(obj) {
    return obj.#name;
  }
}
class Person {
  #name; // (C)
  constructor(name) {
    this.#name = name;
  }
}

assert.equal(
  Color.getName(new Color('green')), 'green'
);

// We can’t access the private slot #name of a Person in line B:
assert.throws(
  () => Color.getName(new Person('Jane')),
  {
    name: 'TypeError',
    message: 'Cannot read private member #name from'
      + ' an object whose class did not declare it',
  }
);
29.2.5.4 プライベートフィールドの名前が衝突することはない

サブクラスがプライベートフィールドに同じ名前を使用した場合でも、両方の名前はプライベート名(常に一意)を参照するため、衝突することはありません。次の例では、SuperClass.#privateFieldは、両方のスロットがinstに直接格納されている場合でも、SubClass.#privateFieldと衝突しません。

class SuperClass {
  #privateField = 'super';
  getSuperPrivateField() {
    return this.#privateField;
  }
}
class SubClass extends SuperClass {
  #privateField = 'sub';
  getSubPrivateField() {
    return this.#privateField;
  }
}
const inst = new SubClass();
assert.equal(
  inst.getSuperPrivateField(), 'super'
);
assert.equal(
  inst.getSubPrivateField(), 'sub'
);

extendsによるサブクラス化については、この章の後半で説明します。

29.2.5.5 inを使用してオブジェクトに指定されたプライベートスロットがあるかどうかを確認する

in演算子を使用して、プライベートスロットが存在するかどうかを確認できます(A行)。

class Color {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj; // (A)
  }
}

プライベートスロットに適用されたinのさらに多くの例を見てみましょう。

プライベートメソッド。次のコードは、プライベートメソッドがインスタンスにプライベートスロットを作成することを示しています。

class C1 {
  #priv() {}
  static check(obj) {
    return #priv in obj;
  }
}
assert.equal(C1.check(new C1()), true);

静的プライベートフィールド。静的プライベートフィールドにもinを使用できます。

class C2 {
  static #priv = 1;
  static check(obj) {
    return #priv in obj;
  }
}
assert.equal(C2.check(C2), true);
assert.equal(C2.check(new C2()), false);

静的プライベートメソッド。静的プライベートメソッドのスロットも確認できます。

class C3 {
  static #priv() {}
  static check(obj) {
    return #priv in obj;
  }
}
assert.equal(C3.check(C3), true);

異なるクラスで同じプライベート識別子を使用する。次の例では、ColorクラスとPersonクラスの両方に、識別子が#nameであるスロットがあります。in演算子はそれらを正しく区別します。

class Color {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}
class Person {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}

// Detecting Color’s #name
assert.equal(
  Color.check(new Color()), true
);
assert.equal(
  Color.check(new Person()), false
);

// Detecting Person’s #name
assert.equal(
  Person.check(new Person()), true
);
assert.equal(
  Person.check(new Color()), false
);

29.2.6 JavaScriptにおけるクラスの長所と短所

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

クラスが完璧であるという意味ではありません。

これはクラスの最初の概要です。すぐにさらに多くの機能を探ります。

  演習:クラスの作成

exercises/classes/point_class_test.mjs

29.2.7 クラスを使用するためのヒント

29.3 クラスの内部構造

29.3.1 クラスは実際には2つの関連付けられたオブジェクトである

内部的には、クラスは2つの関連付けられたオブジェクトになります。クラスPersonを再考して、それがどのように機能するかを見てみましょう。

class Person {
  #firstName;
  constructor(firstName) {
    this.#firstName = firstName;
  }
  describe() {
    return `Person named ${this.#firstName}`;
  }
  static extractNames(persons) {
    return persons.map(person => person.#firstName);
  }
}

クラスによって作成された最初のオブジェクトは、Personに格納されます。4つのプロパティがあります。

assert.deepEqual(
  Reflect.ownKeys(Person),
  ['length', 'name', 'prototype', 'extractNames']
);

// The number of parameters of the constructor
assert.equal(
  Person.length, 1
);

// The name of the class
assert.equal(
  Person.name, 'Person'
);

残りの2つのプロパティは次のとおりです。

これらはPerson.prototypeの内容です。

assert.deepEqual(
  Reflect.ownKeys(Person.prototype),
  ['constructor', 'describe']
);

2つのプロパティがあります。

29.3.2 クラスはインスタンスのプロトタイプチェーンを設定する

オブジェクトPerson.prototypeは、すべてのインスタンスのプロトタイプです。

const jane = new Person('Jane');
assert.equal(
  Object.getPrototypeOf(jane), Person.prototype
);

const tarzan = new Person('Tarzan');
assert.equal(
  Object.getPrototypeOf(tarzan), Person.prototype
);

これにより、インスタンスがメソッドを取得する方法が説明されます。オブジェクトPerson.prototypeから継承します。

13は、すべてがどのように接続されているかを視覚的に示しています。

Figure 13: The class Person has the property .prototype that points to an object that is the prototype of all instances of Person. The objects jane and tarzan are two such instances.

29.3.3 .__proto__.prototype

.__proto__.prototypeを混同しやすいです。図13は、それらがどのように異なるかを明確に示しています。

29.3.4 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
assert.equal(cheeta instanceof Person, true);

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

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

29.3.5 ディスパッチされたメソッド呼び出しと直接メソッド呼び出し(上級)

このサブセクションでは、メソッドを呼び出す2つの異なる方法について学習します。

両方を理解することで、メソッドの動作に関する重要な洞察が得られます。

この章の後半でも2番目の方法が必要になります。これにより、Object.prototypeから便利なメソッドを借用できます。

29.3.5.1 ディスパッチされたメソッド呼び出し

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

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

14には、janeのプロトタイプチェーンを示す図があります。

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

通常のメソッド呼び出しはディスパッチされます。メソッド呼び出し

jane.describe()

は2つのステップで行われます。

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

29.3.5.2 直接メソッド呼び出し

ディスパッチせずに、メソッドを直接呼び出すこともできます。

Person.prototype.describe.call(jane)

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

  thisは常にインスタンスを指す

メソッドがインスタンスのプロトタイプチェーンのどこに配置されていても、thisは常にインスタンス(プロトタイプチェーンの先頭)を指します。これにより、例では.describe().#firstNameにアクセスできます。

直接メソッド呼び出しはいつ役立つでしょうか?特定のオブジェクトが持っていない他の場所からメソッドを借りたい場合など、次の場合です。

const obj = Object.create(null);

// `obj` is not an instance of Object and doesn’t inherit
// its prototype method .toString()
assert.throws(
  () => obj.toString(),
  /^TypeError: obj.toString is not a function$/
);
assert.equal(
  Object.prototype.toString.call(obj),
  '[object Object]'
);

29.3.6 クラスは通常の関数から進化した(上級)

ECMAScript 6より前は、JavaScriptにはクラスがありませんでした。代わりに、通常の関数コンストラクタ関数として使用されていました。

function StringBuilderConstr(initialString) {
  this.string = initialString;
}
StringBuilderConstr.prototype.add = function (str) {
  this.string += str;
  return this;
};

const sb = new StringBuilderConstr('¡');
sb.add('Hola').add('!');
assert.equal(
  sb.string, '¡Hola!'
);

クラスはこのアプローチにより優れた構文を提供します。

class StringBuilderClass {
  constructor(initialString) {
    this.string = initialString;
  }
  add(str) {
    this.string += str;
    return this;
  }
}
const sb = new StringBuilderClass('¡');
sb.add('Hola').add('!');
assert.equal(
  sb.string, '¡Hola!'
);

サブクラス化は、コンストラクタ関数では特に困難です。クラスは、より便利な構文を超えた利点も提供します。

クラスはコンストラクタ関数と非常に互換性があるため、それらを拡張することさえできます。

function SuperConstructor() {}
class SubClass extends SuperConstructor {}

assert.equal(
  new SubClass() instanceof SuperConstructor, true
);

extendsとサブクラス化については、この章の後半で説明します

29.3.6.1 クラスはコンストラクタである

これは興味深い洞察につながります。一方では、StringBuilderClassStringBuilderClass.prototype.constructorを介してコンストラクタを参照します。

一方、クラスはコンストラクタ(関数)です。

> StringBuilderClass.prototype.constructor === StringBuilderClass
true
> typeof StringBuilderClass
'function'

  コンストラクタ(関数)とクラス

それらが非常に似ているため、「コンストラクタ(関数)」と「クラス」という用語を互換的に使用しています。

29.4 クラスのプロトタイプメンバー

29.4.1 パブリックプロトタイプメソッドとアクセサ

次のクラス宣言の本体内のすべてのメンバーは、PublicProtoClass.prototypeのプロパティを作成します。

class PublicProtoClass {
  constructor(args) {
    // (Do something with `args` here.)
  }
  publicProtoMethod() {
    return 'publicProtoMethod';
  }
  get publicProtoAccessor() {
    return 'publicProtoGetter';
  }
  set publicProtoAccessor(value) {
    assert.equal(value, 'publicProtoSetter');
  }
}

assert.deepEqual(
  Reflect.ownKeys(PublicProtoClass.prototype),
  ['constructor', 'publicProtoMethod', 'publicProtoAccessor']
);

const inst = new PublicProtoClass('arg1', 'arg2');
assert.equal(
  inst.publicProtoMethod(), 'publicProtoMethod'
);
assert.equal(
  inst.publicProtoAccessor, 'publicProtoGetter'
);
inst.publicProtoAccessor = 'publicProtoSetter';
29.4.1.1 すべてのパブリックプロトタイプメソッドとアクセサ(上級)
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');

class PublicProtoClass2 {
  // Identifier keys
  get accessor() {}
  set accessor(value) {}
  syncMethod() {}
  * syncGeneratorMethod() {}
  async asyncMethod() {}
  async * asyncGeneratorMethod() {}

  // Quoted keys
  get 'an accessor'() {}
  set 'an accessor'(value) {}
  'sync method'() {}
  * 'sync generator method'() {}
  async 'async method'() {}
  async * 'async generator method'() {}

  // Computed keys
  get [accessorKey]() {}
  set [accessorKey](value) {}
  [syncMethodKey]() {}
  * [syncGenMethodKey]() {}
  async [asyncMethodKey]() {}
  async * [asyncGenMethodKey]() {}
}

// Quoted and computed keys are accessed via square brackets:
const inst = new PublicProtoClass2();
inst['sync method']();
inst[syncMethodKey]();

引用符付きキーと計算されたキーもオブジェクトリテラルで使用できます。

アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報

29.4.2 プライベートメソッドとアクセサ[ES2022]

プライベートメソッド(およびアクセサ)は、プロトタイプメンバーとインスタンスメンバーの興味深い混合です。

一方、プライベートメソッドはインスタンスのスロットに格納されます(A行)。

class MyClass {
  #privateMethod() {}
  static check() {
    const inst = new MyClass();
    assert.equal(
      #privateMethod in inst, true // (A)
    );
    assert.equal(
      #privateMethod in MyClass.prototype, false
    );
    assert.equal(
      #privateMethod in MyClass, false
    );
  }
}
MyClass.check();

なぜ.prototypeオブジェクトに格納されないのですか?プライベートスロットは継承されません。プロパティだけが継承されます。

一方、プライベートメソッドは、パブリックプロトタイプメソッドのように、インスタンス間で共有されます。

class MyClass {
  #privateMethod() {}
  static check() {
    const inst1 = new MyClass();
    const inst2 = new MyClass();
    assert.equal(
      inst1.#privateMethod,
      inst2.#privateMethod
    );
  }
}

そのため、および構文がパブリックプロトタイプメソッドと似ているため、ここで説明します。

次のコードは、プライベートメソッドとアクセサの動作を示しています。

class PrivateMethodClass {
  #privateMethod() {
    return 'privateMethod';
  }
  get #privateAccessor() {
    return 'privateGetter';
  }
  set #privateAccessor(value) {
    assert.equal(value, 'privateSetter');
  }
  callPrivateMembers() {
    assert.equal(this.#privateMethod(), 'privateMethod');
    assert.equal(this.#privateAccessor, 'privateGetter');
    this.#privateAccessor = 'privateSetter';
  }
}
assert.deepEqual(
  Reflect.ownKeys(new PrivateMethodClass()), []
);
29.4.2.1 すべてのパブリックプロトタイプメソッドとアクセサ(上級)

プライベートスロットでは、キーは常に識別子です。

class PrivateMethodClass2 {
  get #accessor() {}
  set #accessor(value) {}
  #syncMethod() {}
  * #syncGeneratorMethod() {}
  async #asyncMethod() {}
  async * #asyncGeneratorMethod() {}
}

アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報

29.5 クラスのインスタンスメンバー[ES2022]

29.5.1 インスタンスパブリックフィールド

次のクラスのインスタンスには、2つのインスタンスプロパティがあります(A行とB行で作成)。

class InstPublicClass {
  // Instance public field
  instancePublicField = 0; // (A)

  constructor(value) {
    // We don’t need to mention .property elsewhere!
    this.property = value; // (B)
  }
}

const inst = new InstPublicClass('constrArg');
assert.deepEqual(
  Reflect.ownKeys(inst),
  ['instancePublicField', 'property']
);
assert.equal(
  inst.instancePublicField, 0
);
assert.equal(
  inst.property, 'constrArg'
);

コンストラクタ内でインスタンスプロパティを作成する場合(B行)、他の場所で「宣言」する必要はありません。すでに見たように、インスタンスプライベートフィールドとは異なります。

JavaScriptにおけるインスタンスプロパティは比較的一般的です。例えば、Javaのようにほとんどのインスタンス状態がプライベートである場合と比較して、はるかに一般的です。

29.5.1.1 クォートされたキーと計算されたキーを持つインスタンスパブリックフィールド(上級)
const computedFieldKey = Symbol('computedFieldKey');
class InstPublicClass2 {
  'quoted field key' = 1;
  [computedFieldKey] = 2;
}
const inst = new InstPublicClass2();
assert.equal(inst['quoted field key'], 1);
assert.equal(inst[computedFieldKey], 2);
29.5.1.2 インスタンスパブリックフィールドにおける`this`の値は何ですか?(上級)

インスタンスパブリックフィールドのイニシャライザにおいて、`this`は新しく作成されたインスタンスを参照します。

class MyClass {
  instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
  inst.instancePublicField, inst
);
29.5.1.3 インスタンスパブリックフィールドはいつ実行されますか?(上級)

インスタンスパブリックフィールドの実行は、おおよそ次の2つのルールに従います。

次の例はこれらのルールを示しています。

class SuperClass {
  superProp = console.log('superProp');
  constructor() {
    console.log('super-constructor');
  }
}
class SubClass extends SuperClass {
  subProp = console.log('subProp');
  constructor() {
    console.log('BEFORE super()');
    super();
    console.log('AFTER super()');
  }
}
new SubClass();

// Output:
// 'BEFORE super()'
// 'superProp'
// 'super-constructor'
// 'subProp'
// 'AFTER super()'

extendsとサブクラス化については、この章の後半で説明します

29.5.2 インスタンスプライベートフィールド

次のクラスには、2つのインスタンスプライベートフィールドが含まれています(A行とB行)。

class InstPrivateClass {
  #privateField1 = 'private field 1'; // (A)
  #privateField2; // (B) required!
  constructor(value) {
    this.#privateField2 = value; // (C)
  }
  /**
   * Private fields are not accessible outside the class body.
   */
  checkPrivateValues() {
    assert.equal(
      this.#privateField1, 'private field 1'
    );
    assert.equal(
      this.#privateField2, 'constructor argument'
    );
  }
}

const inst = new InstPrivateClass('constructor argument');
  inst.checkPrivateValues();

// No instance properties were created
assert.deepEqual(
  Reflect.ownKeys(inst),
  []
);

クラス本体で`.#privateField2`を宣言した場合のみ、C行でそれを使用できることに注意してください。

29.5.3 ES2022以前のプライベートインスタンスデータ(上級)

このセクションでは、インスタンスデータをプライベートに保持するための2つの手法を見ていきます。これらはクラスに依存しないため、オブジェクトリテラルなど、他の方法で作成されたオブジェクトにも使用できます。

29.5.3.1 ES6以前:命名規則によるプライベートメンバ

最初のテクニックは、プロパティ名の前にアンダースコアを付けることでプロパティをプライベートにします。これはプロパティを何らかの方法で保護するものではありません。単に外部に対して「このプロパティを知る必要はありません」という合図を送るだけです。

次のコードでは、プロパティ`._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.5.3.2 ES6以降:WeakMapによるプライベートインスタンスデータ

WeakMapを使用してプライベートインスタンスデータを管理することもできます。

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()),
  []);

その仕組みは、WeakMapに関する章で説明されています。

この手法は、外部からのアクセスからかなりの保護を提供し、名前の衝突もありません。しかし、使用するのもより複雑です。

擬似プロパティ`_superProp`の可視性を、誰がアクセスできるかを制御することで制御します。たとえば、変数がモジュール内に存在し、エクスポートされていない場合、モジュール内のすべての人がアクセスでき、モジュール外の誰もアクセスできません。言い換えれば、この場合のプライバシーの範囲はクラスではなくモジュールです。ただし、範囲を狭めることができます。

let Countdown;
{ // class scope
  const _counter = new WeakMap();
  const _action = new WeakMap();

  Countdown = class {
    // ···
  }
}

この手法は、プライベートメソッドを実際にはサポートしていません。しかし、`_superProp`にアクセスできるモジュールローカル関数は、それに次ぐ最良の方法です。

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

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    privateDec(this);
  }
}

function privateDec(_this) { // (A)
  let counter = _counter.get(_this);
  counter--;
  _counter.set(_this, counter);
  if (counter === 0) {
    _action.get(_this)();
  }
}

`this`は、明示的な関数パラメータ`_this`になります(A行)。

29.5.4 WeakMapを使用した保護された可視性とフレンド可視性のシミュレーション(上級)

前述のように、インスタンスプライベートフィールドは、そのクラス内でのみ可視であり、サブクラス内でも可視ではありません。したがって、次のものを取得するための組み込みの方法はありません。

前のサブセクションでは、WeakMapを使用して「モジュール可視性」(モジュール内のすべての人がインスタンスデータの一部にアクセスできる)をシミュレートしました。したがって、

次の例は、保護された可視性を示しています。

const _superProp = new WeakMap();
class SuperClass {
  constructor() {
    _superProp.set(this, 'superProp');
  }
}
class SubClass extends SuperClass {
  getSuperProp() {
    return _superProp.get(this);
  }
}
assert.equal(
  new SubClass().getSuperProp(),
  'superProp'
);

extendsによるサブクラス化については、この章の後半で説明します。

29.6 クラスの静的メンバ

29.6.1 静的公開メソッドとアクセサ

次のクラス宣言の本体内のすべてのメンバは、いわゆる静的プロパティを作成します。`StaticClass`自体のプロパティです。

class StaticPublicMethodsClass {
  static staticMethod() {
    return 'staticMethod';
  }
  static get staticAccessor() {
    return 'staticGetter';
  }
  static set staticAccessor(value) {
    assert.equal(value, 'staticSetter');
  }
}
assert.equal(
  StaticPublicMethodsClass.staticMethod(), 'staticMethod'
);
assert.equal(
  StaticPublicMethodsClass.staticAccessor, 'staticGetter'
);
StaticPublicMethodsClass.staticAccessor = 'staticSetter';
29.6.1.1 すべての種類の静的公開メソッドとアクセサ(上級)
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');

class StaticPublicMethodsClass2 {
  // Identifier keys
  static get accessor() {}
  static set accessor(value) {}
  static syncMethod() {}
  static * syncGeneratorMethod() {}
  static async asyncMethod() {}
  static async * asyncGeneratorMethod() {}

  // Quoted keys
  static get 'an accessor'() {}
  static set 'an accessor'(value) {}
  static 'sync method'() {}
  static * 'sync generator method'() {}
  static async 'async method'() {}
  static async * 'async generator method'() {}

  // Computed keys
  static get [accessorKey]() {}
  static set [accessorKey](value) {}
  static [syncMethodKey]() {}
  static * [syncGenMethodKey]() {}
  static async [asyncMethodKey]() {}
  static async * [asyncGenMethodKey]() {}
}

// Quoted and computed keys are accessed via square brackets:
StaticPublicMethodsClass2['sync method']();
StaticPublicMethodsClass2[syncMethodKey]();

引用符付きキーと計算されたキーもオブジェクトリテラルで使用できます。

アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報

29.6.2 静的公開フィールド[ES2022]

次のコードは静的公開フィールドを示しています。`StaticPublicFieldClass`には3つの静的公開フィールドがあります。

const computedFieldKey = Symbol('computedFieldKey');
class StaticPublicFieldClass {
  static identifierFieldKey = 1;
  static 'quoted field key' = 2;
  static [computedFieldKey] = 3;
}

assert.deepEqual(
  Reflect.ownKeys(StaticPublicFieldClass),
  [
    'length', // number of constructor parameters
    'name', // 'StaticPublicFieldClass'
    'prototype',
    'identifierFieldKey',
    'quoted field key',
    computedFieldKey,
  ],
);

assert.equal(StaticPublicFieldClass.identifierFieldKey, 1);
assert.equal(StaticPublicFieldClass['quoted field key'], 2);
assert.equal(StaticPublicFieldClass[computedFieldKey], 3);

29.6.3 静的プライベートメソッド、アクセサ、およびフィールド[ES2022]

次のクラスには、2つの静的プライベートスロットがあります(A行とB行)。

class StaticPrivateClass {
  // Declare and initialize
  static #staticPrivateField = 'hello'; // (A)
  static #twice() { // (B)
    const str = StaticPrivateClass.#staticPrivateField;
    return str + ' ' + str;
  }
  static getResultOfTwice() {
    return StaticPrivateClass.#twice();
  }
}

assert.deepEqual(
  Reflect.ownKeys(StaticPrivateClass),
  [
    'length', // number of constructor parameters
    'name', // 'StaticPublicFieldClass'
    'prototype',
    'getResultOfTwice',
  ],
);

assert.equal(
  StaticPrivateClass.getResultOfTwice(),
  'hello hello'
);

これは、すべての種類の静的プライベートスロットの完全なリストです。

class MyClass {
  static #staticPrivateMethod() {}
  static * #staticPrivateGeneratorMethod() {}

  static async #staticPrivateAsyncMethod() {}
  static async * #staticPrivateAsyncGeneratorMethod() {}
  
  static get #staticPrivateAccessor() {}
  static set #staticPrivateAccessor(value) {}
}

29.6.4 クラスの静的初期化ブロック[ES2022]

クラスを使用してインスタンスデータを設定するには、2つの構成要素があります。

静的データの場合、

次のコードは静的ブロックを示しています(A行)。

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static { // (A)
    for (const [english, german] of Object.entries(this.translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
  }
}

クラスの後(トップレベル)で静的ブロック内のコードを実行することもできます。しかし、静的ブロックを使用することには2つの利点があります。

29.6.4.1 静的初期化ブロックのルール

静的初期化ブロックの動作に関するルールは比較的単純です。

次のコードはこれらのルールを示しています。

class SuperClass {
  static superField1 = console.log('superField1');
  static {
    assert.equal(this, SuperClass);
    console.log('static block 1 SuperClass');
  }
  static superField2 = console.log('superField2');
  static {
    console.log('static block 2 SuperClass');
  }
}

class SubClass extends SuperClass {
  static subField1 = console.log('subField1');
  static {
    assert.equal(this, SubClass);
    console.log('static block 1 SubClass');
  }
  static subField2 = console.log('subField2');
  static {
    console.log('static block 2 SubClass');
  }
}

// Output:
// 'superField1'
// 'static block 1 SuperClass'
// 'superField2'
// 'static block 2 SuperClass'
// 'subField1'
// 'static block 1 SubClass'
// 'subField2'
// 'static block 2 SubClass'

extendsによるサブクラス化については、この章の後半で説明します。

29.6.5 陥り穴:静的プライベートフィールドにアクセスするための`this`の使用

静的公開メンバでは、`this`を使用して静的公開スロットにアクセスできます。残念ながら、静的プライベートスロットにアクセスするためにそれを使用するべきではありません。

29.6.5.1 `this`と静的公開フィールド

次のコードを考えてみましょう。

class SuperClass {
  static publicData = 1;
  
  static getPublicViaThis() {
    return this.publicData;
  }
}
class SubClass extends SuperClass {
}

extendsによるサブクラス化については、この章の後半で説明します。

静的公開フィールドはプロパティです。メソッド呼び出しを行うと、

assert.equal(SuperClass.getPublicViaThis(), 1);

`this`は`SuperClass`を指し、すべてが期待通りに動作します。サブクラスを介して` .getPublicViaThis()`を呼び出すこともできます。

assert.equal(SubClass.getPublicViaThis(), 1);

`SubClass`はプロトタイプ`SuperClass`から` .getPublicViaThis()`を継承します。`this`は`SubClass`を指し、`SubClass`もプロパティ` .publicData`を継承するため、動作し続けます。

補足として、`getPublicViaThis()`で`this.publicData`に代入し、`SubClass.getPublicViaThis()`を介して呼び出した場合、`SuperClass`から継承されたプロパティを(非破壊的に)オーバーライドする`SubClass`の新しい独自の プロパティが作成されます。

29.6.5.2 `this`と静的プライベートフィールド

次のコードを考えてみましょう。

class SuperClass {
  static #privateData = 2;
  static getPrivateDataViaThis() {
    return this.#privateData;
  }
  static getPrivateDataViaClassName() {
    return SuperClass.#privateData;
  }
}
class SubClass extends SuperClass {
}

`SuperClass`を介して` .getPrivateDataViaThis()`を呼び出すと機能します。なぜなら、`this`は`SuperClass`を指しているからです。

assert.equal(SuperClass.getPrivateDataViaThis(), 2);

ただし、`SubClass`を介して` .getPrivateDataViaThis()`を呼び出すと機能しません。なぜなら、`this`は今度は`SubClass`を指し、`SubClass`には静的プライベートフィールド`.#privateData`がないからです(プロトタイプチェーンのプライベートスロットは継承されません)。

assert.throws(
  () => SubClass.getPrivateDataViaThis(),
  {
    name: 'TypeError',
    message: 'Cannot read private member #privateData from'
      + ' an object whose class did not declare it',
  }
);

回避策は、`SuperClass`を介して`.#privateData`に直接アクセスすることです。

assert.equal(SubClass.getPrivateDataViaClassName(), 2);

静的プライベートメソッドでも、同じ問題に直面します。

29.6.6 すべてのメンバ(静的、プロトタイプ、インスタンス)はすべてのプライベートメンバにアクセスできます

クラス内のすべてのメンバは、そのクラス内の他のすべてのメンバ(公開とプライベートの両方)にアクセスできます。

class DemoClass {
  static #staticPrivateField = 1;
  #instPrivField = 2;

  static staticMethod(inst) {
    // A static method can access static private fields
    // and instance private fields
    assert.equal(DemoClass.#staticPrivateField, 1);
    assert.equal(inst.#instPrivField, 2);
  }

  protoMethod() {
    // A prototype method can access instance private fields
    // and static private fields
    assert.equal(this.#instPrivField, 2);
    assert.equal(DemoClass.#staticPrivateField, 1);
  }
}

対照的に、外部からはプライベートメンバにアクセスできません。

// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
  () => eval('DemoClass.#staticPrivateField'),
  {
    name: 'SyntaxError',
    message: "Private field '#staticPrivateField' must"
      + " be declared in an enclosing class",
  }
);
// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
  () => eval('new DemoClass().#instPrivField'),
  {
    name: 'SyntaxError',
    message: "Private field '#instPrivField' must"
      + " be declared in an enclosing class",
  }
);

29.6.7 ES2022以前の静的プライベートメソッドとデータ

次のコードは、ハッシュ記号(`#`)を含むすべての行が原因で、ES2022でのみ機能します。

class StaticClass {
  static #secret = 'Rumpelstiltskin';
  static #getSecretInParens() {
    return `(${StaticClass.#secret})`;
  }
  static callStaticPrivateMethod() {
    return StaticClass.#getSecretInParens();
  }
}

プライベートスロットはクラスごとに1回しか存在しないため、`#secret`と`#getSecretInParens`をクラスを囲むスコープに移動し、モジュールを使用してそれらをモジュール外の外部の世界から隠すことができます。

const secret = 'Rumpelstiltskin';
function getSecretInParens() {
  return `(${secret})`;
}

// Only the class is accessible outside the module
export class StaticClass {
  static callStaticPrivateMethod() {
    return getSecretInParens();
  }
}

29.6.8 静的ファクトリメソッド

クラスをインスタンス化する方法は複数ある場合があります。その場合、`Point.fromPolar()`などの静的ファクトリメソッドを実装できます。

class Point {
  static fromPolar(radius, angle) {
    const x = radius * Math.cos(angle);
    const y = radius * Math.sin(angle);
    return new Point(x, y);
  }
  constructor(x=0, y=0) {
    this.x = x;
    this.y = y;
  }
}

assert.deepEqual(
  Point.fromPolar(13, 0.39479111969976155),
  new Point(12, 5)
);

静的ファクトリメソッドがどれほど記述的であるかが気に入っています。`fromPolar`はインスタンスの作成方法を記述しています。JavaScriptの標準ライブラリにもそのようなファクトリメソッドがあります。たとえば、

静的ファクトリメソッドを全く持たないか、のみ静的ファクトリメソッドを持つ方が好きです。後者の場合に考慮すべき点

次のコードでは、秘密のトークン(A行)を使用して、現在のモジュール外部からコンストラクタが呼び出されないようにしています。

// Only accessible inside the current module
const secretToken = Symbol('secretToken'); // (A)

export class Point {
  static create(x=0, y=0) {
    return new Point(secretToken, x, y);
  }
  static fromPolar(radius, angle) {
    const x = radius * Math.cos(angle);
    const y = radius * Math.sin(angle);
    return new Point(secretToken, x, y);
  }
  constructor(token, x, y) {
    if (token !== secretToken) {
      throw new TypeError('Must use static factory method');
    }
    this.x = x;
    this.y = y;
  }
}
Point.create(3, 4); // OK
assert.throws(
  () => new Point(3, 4),
  TypeError
);

29.7 サブクラス化

クラスは既存のクラスを拡張することもできます。たとえば、次のクラス`Employee`は`Person`を拡張します。

class Person {
  #firstName;
  constructor(firstName) {
    this.#firstName = firstName;
  }
  describe() {
    return `Person named ${this.#firstName}`;
  }
  static extractNames(persons) {
    return persons.map(person => person.#firstName);
  }
}

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

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

拡張に関連する用語

派生クラスの`.constructor()`内では、`this`にアクセスする前に、`super()`を介してスーパークラスコンストラクタを呼び出す必要があります。なぜでしょうか?

クラスのチェーンを考えてみましょう。

`new C()`を呼び出すと、`C`のコンストラクタは`B`のコンストラクタをスーパークラスとして呼び出し、`B`のコンストラクタは`A`のコンストラクタをスーパークラスとして呼び出します。インスタンスは常に基底クラスで作成され、サブクラスのコンストラクタがスロットを追加する前に作成されます。したがって、`super()`を呼び出す前にはインスタンスは存在せず、`this`を介してまだアクセスできません。

静的公開スロットは継承されることに注意してください。たとえば、`Employee`は静的メソッド`.extractNames()`を継承します。

> 'extractNames' in Employee
true

  演習:サブクラス化

exercises/classes/color_point_class_test.mjs

29.7.1 サブクラス化の内情(上級)

Figure 15: 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.

前のセクションのクラス`Person`と`Employee`は、いくつかのオブジェクトで構成されています(図15)。これらのオブジェクトの関係を理解するための重要な洞察の1つは、2つのプロトタイプチェーンが存在することです。

29.7.1.1 インスタンスプロトタイプチェーン(右列)

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

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

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

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

29.7.2 instanceofとサブクラス化(上級)

instanceofが実際にどのように動作するかはまだ学習していません。instanceofは、値xがクラスCのインスタンスであるかどうか(Cの直接インスタンス、またはCのサブクラスの直接インスタンスのいずれか)をどのように判断しますか?それは、C.prototypexのプロトタイプチェーン内にあるかどうかを確認します。つまり、次の2つの式は同等です。

x instanceof C
C.prototype.isPrototypeOf(x)

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

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

instanceofは、左辺がプリミティブ値の場合、常にfalseを返すことに注意してください。

> 'abc' instanceof String
false
> 123 instanceof Number
false

29.7.3 すべてのオブジェクトがObjectのインスタンスであるわけではない(上級)

オブジェクト(プリミティブではない値)は、Object.prototypeがそのプロトタイプチェーン内にある場合にのみ、Objectのインスタンスです (前のセクションを参照)。事実上すべてのオブジェクトはObjectのインスタンスです。例えば

assert.equal(
  {a: 1} instanceof Object, true
);
assert.equal(
  ['a'] instanceof Object, true
);
assert.equal(
  /abc/g instanceof Object, true
);
assert.equal(
  new Map() instanceof Object, true
);

class C {}
assert.equal(
  new C() instanceof Object, true
);

次の例では、obj1obj2はどちらもオブジェクトです(A行とC行)。しかし、それらはObjectのインスタンスではありません(B行とD行)。Object.prototypeは、プロトタイプを持たないため、それらのプロトタイプチェーンにはありません。

const obj1 = {__proto__: null};
assert.equal(
  typeof obj1, 'object' // (A)
);
assert.equal(
  obj1 instanceof Object, false // (B)
);

const obj2 = Object.create(null);
assert.equal(
  typeof obj2, 'object' // (C)
);
assert.equal(
  obj2 instanceof Object, false // (D)
);

Object.prototypeは、ほとんどのプロトタイプチェーンを終了するオブジェクトです。そのプロトタイプはnullであるため、それ自体もObjectのインスタンスではありません。

> typeof Object.prototype
'object'
> Object.getPrototypeOf(Object.prototype)
null
> Object.prototype instanceof Object
false

29.7.4 組み込みオブジェクトのプロトタイプチェーン(上級)

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

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

Object.getPrototypeOf()メソッドを抽出し、pに代入しました。

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

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

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

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

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

配列のプロトタイプチェーンはどうなっていますか?

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

このプロトタイプチェーン(図17に可視化されています)は、配列オブジェクトがArrayObjectのインスタンスであることを示しています。

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

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

> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true
29.7.4.4 組み込みクラスのプロトタイプチェーン

基底クラスのプロトタイプはFunction.prototypeであるため、それは関数(Functionのインスタンス)です。

class A {}
assert.equal(
  Object.getPrototypeOf(A),
  Function.prototype
);

assert.equal(
  Object.getPrototypeOf(class {}),
  Function.prototype
);

派生クラスのプロトタイプは、そのスーパークラスです。

class B extends A {}
assert.equal(
  Object.getPrototypeOf(B),
  A
);

assert.equal(
  Object.getPrototypeOf(class extends Object {}),
  Object
);

興味深いことに、ObjectArrayFunctionはすべて基底クラスです。

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

しかし、見てきたように、基底クラスのインスタンスでさえ、すべてのオブジェクトに必要なサービスを提供するため、プロトタイプチェーンにObject.prototypeが含まれています。

  なぜArrayFunctionは基底クラスなのですか?

基底クラスは、インスタンスが実際に作成される場所です。ArrayFunctionはどちらも、後でObjectによって作成されたインスタンスに追加できない「内部スロット」と呼ばれるものを持っているため、独自のインスタンスを作成する必要があります。

29.7.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)) {
  /*···*/
}

これで、S2()によって返されたクラスを拡張し、S1()によって返されたクラスを拡張し、Objectを拡張するクラスCができました。

29.7.5.1 例:ブランド管理のためのミキシン

オブジェクトのブランドの設定と取得のためのヘルパーメソッドを持つミキシンBrandedを実装します。

const Named = (Sup) => class extends Sup {
  name = '(Unnamed)';
  toString() {
    const className = this.constructor.name;
    return `${className} named ${this.name}`;
  }
};

このミキシンを使用して、名前を持つクラスCityを実装します。

class City extends Named(Object) {
  constructor(name) {
    super();
    this.name = name;
  }
}

次のコードは、ミキシンが機能することを確認します。

const paris = new City('Paris');
assert.equal(
  paris.name, 'Paris'
);
assert.equal(
  paris.toString(), 'City named Paris'
);
29.7.5.2 ミキシンの利点

ミキシンは、単一継承の制約から解放します。

29.8 Object.prototypeのメソッドとアクセサ(上級)

§29.7.3 「すべてのオブジェクトがObjectのインスタンスであるわけではない」で見たように、ほとんどすべてのオブジェクトはObjectのインスタンスです。このクラスは、インスタンスにいくつかの便利なメソッドとアクセサを提供します。

これらの機能を詳しく見ていく前に、重要な落とし穴(とその回避方法)について学習します。Object.prototypeの機能をすべてのオブジェクトで使用することはできません。

29.8.1 Object.prototypeメソッドを安全に使用する方法

任意のオブジェクトでObject.prototypeのメソッドの1つを呼び出しても、常に機能するとは限りません。その理由を説明するために、オブジェクトが指定されたキーを持つ独自の特性を持っている場合にtrueを返すメソッドObject.prototype.hasOwnPropertyを使用します。

> {ownProp: true}.hasOwnProperty('ownProp')
true
> {ownProp: true}.hasOwnProperty('abc')
false

任意のオブジェクトで.hasOwnProperty()を呼び出すと、2つの方法で失敗する可能性があります。一方、オブジェクトがObjectのインスタンスでない場合、このメソッドは使用できません(§29.7.3 「すべてのオブジェクトがObjectのインスタンスであるわけではない」を参照)。

const obj = Object.create(null);
assert.equal(obj instanceof Object, false);
assert.throws(
  () => obj.hasOwnProperty('prop'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);

もう一方、オブジェクトが独自の特性でそれをオーバーライドする場合(A行)、.hasOwnProperty()を使用できません。

const obj = {
  hasOwnProperty: 'yes' // (A)
};
assert.throws(
  () => obj.hasOwnProperty('prop'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);

しかし、.hasOwnProperty()を安全に使用する方法があります。

function hasOwnProp(obj, propName) {
  return Object.prototype.hasOwnProperty.call(obj, propName); // (A)
}
assert.equal(
  hasOwnProp(Object.create(null), 'prop'), false
);
assert.equal(
  hasOwnProp({hasOwnProperty: 'yes'}, 'prop'), false
);
assert.equal(
  hasOwnProp({hasOwnProperty: 'yes'}, 'hasOwnProperty'), true
);

A行のメソッド呼び出しについては、§29.3.5 「ディスパッチされたメソッド呼び出しと直接メソッド呼び出し」で説明されています。

.bind()を使用してhasOwnProp()を実装することもできます。

const hasOwnProp = Object.prototype.hasOwnProperty.call
  .bind(Object.prototype.hasOwnProperty);

これはどのように機能しますか?前の例でA行のように.call()を呼び出すと、落とし穴を回避することも含め、hasOwnProp()が実行する必要があることを正確に実行します。ただし、関数呼び出ししたい場合は、単純に抽出するのではなく、そのthisが常に正しい値を持つようにする必要があります。それが.bind()の役割です。

  動的ディスパッチを介してObject.prototypeメソッドを使用することは決して許されないのですか?

場合によっては、怠惰になり、Object.prototypeメソッドを通常のメソッドのように(.call()または.bind()なしで)呼び出すことができます。レシーバがわかっていて、固定レイアウトオブジェクトである場合です。

一方、レシーバがわからない場合や、辞書オブジェクトである場合は、注意が必要です。

29.8.2 Object.prototype.toString()

.toString()(サブクラスまたはインスタンスで)をオーバーライドすることにより、オブジェクトが文字列に変換される方法を設定できます。

> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]'

オブジェクトを文字列に変換するには、String()を使用する方が良いでしょう。これはundefinednullでも機能するからです。

> undefined.toString()
TypeError: Cannot read properties of undefined (reading 'toString')
> null.toString()
TypeError: Cannot read properties of null (reading 'toString')
> String(undefined)
'undefined'
> String(null)
'null'

29.8.3 Object.prototype.toLocaleString()

.toLocaleString()は、ロケールと多くの追加オプションで設定できる.toString()のバージョンです。任意のクラスまたはインスタンスがこのメソッドを実装できます。標準ライブラリでは、次のクラスが実装しています。

例として、小数を含む数値は、ロケール('fr'はフランス語、'en'は英語)によって異なる文字列に変換されます。

> 123.45.toLocaleString('fr')
'123,45'
> 123.45.toLocaleString('en')
'123.45'

29.8.4 Object.prototype.valueOf()

.valueOf()(サブクラスまたはインスタンスで)をオーバーライドすることにより、オブジェクトが文字列以外の値(多くの場合、数値)に変換される方法を設定できます。

> Number({valueOf() { return 123 }})
123
> Number({})
NaN

29.8.5 Object.prototype.isPrototypeOf()

proto.isPrototypeOf(obj) は、protoobj のプロトタイプチェーンに存在する場合は true を、そうでない場合は false を返します。

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.8.1「Object.prototype メソッドの安全な使用方法」 を参照してください)。

const obj = {
  // Overrides Object.prototype.isPrototypeOf
  isPrototypeOf: true,
};
// Doesn’t work in this case:
assert.throws(
  () => obj.isPrototypeOf(Object.prototype),
  {
    name: 'TypeError',
    message: 'obj.isPrototypeOf is not a function',
  }
);
// Safe way of using .isPrototypeOf():
assert.equal(
  Object.prototype.isPrototypeOf.call(obj, Object.prototype), false
);

29.8.6 Object.prototype.propertyIsEnumerable()

obj.propertyIsEnumerable(propKey) は、obj がキーが propKey である列挙可能な独自のプロパティを持つ場合は true を、そうでない場合は false を返します。

const proto = {
  enumerableProtoProp: true,
};
const obj = {
  __proto__: proto,
  enumerableObjProp: true,
  nonEnumObjProp: true,
};
Object.defineProperty(
  obj, 'nonEnumObjProp',
  {
    enumerable: false,
  }
);

assert.equal(
  obj.propertyIsEnumerable('enumerableProtoProp'),
  false // not an own property
);
assert.equal(
  obj.propertyIsEnumerable('enumerableObjProp'),
  true
);
assert.equal(
  obj.propertyIsEnumerable('nonEnumObjProp'),
  false // not enumerable
);
assert.equal(
  obj.propertyIsEnumerable('unknownProp'),
  false // not a property
);

このメソッドを安全に使用する方法は次のとおりです(詳細については §29.8.1「Object.prototype メソッドの安全な使用方法」 を参照してください)。

const obj = {
  // Overrides Object.prototype.propertyIsEnumerable
  propertyIsEnumerable: true,
  enumerableProp: 'yes',
};
// Doesn’t work in this case:
assert.throws(
  () => obj.propertyIsEnumerable('enumerableProp'),
  {
    name: 'TypeError',
    message: 'obj.propertyIsEnumerable is not a function',
  }
);
// Safe way of using .propertyIsEnumerable():
assert.equal(
  Object.prototype.propertyIsEnumerable.call(obj, 'enumerableProp'),
  true
);

別の安全な代替手段として、プロパティ記述子 を使用する方法があります。

assert.deepEqual(
  Object.getOwnPropertyDescriptor(obj, 'enumerableProp'),
  {
    value: 'yes',
    writable: true,
    enumerable: true,
    configurable: true,
  }
);

29.8.7 Object.prototype.__proto__(アクセサ)

__proto__ プロパティには2つのバージョンがあります。

前者の機能の使用は避けることをお勧めします。

一方、オブジェクトリテラル内の __proto__ は常に動作し、非推奨ではありません。

アクセサ __proto__ の動作に興味がある場合は、読み進めてください。

__proto__Object.prototype のアクセサであり、すべての Object のインスタンスによって継承されます。クラスを使用して実装すると次のようになります。

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

__proto__Object.prototype から継承されるため、プロトタイプチェーンに Object.prototype を持たないオブジェクトを作成することで、この機能を削除できます(§29.7.3「すべてのオブジェクトが Object のインスタンスであるとは限らない」を参照)。

> '__proto__' in {}
true
> '__proto__' in Object.create(null)
false

29.8.8 Object.prototype.hasOwnProperty()

  .hasOwnProperty() のより良い代替手段:Object.hasOwn() [ES2022]

§28.9.4「Object.hasOwn():指定されたプロパティが独自の(非継承)プロパティかどうかを調べます [ES2022]」を参照してください。

obj.hasOwnProperty(propKey) は、obj がキーが propKey である独自の(非継承)プロパティを持つ場合は true を、そうでない場合は false を返します。

const obj = { ownProp: true };
assert.equal(
  obj.hasOwnProperty('ownProp'), true // own
);
assert.equal(
  'toString' in obj, true // inherited
);
assert.equal(
  obj.hasOwnProperty('toString'), false
);

このメソッドを安全に使用する方法は次のとおりです(詳細については §29.8.1「Object.prototype メソッドの安全な使用方法」 を参照してください)。

const obj = {
  // Overrides Object.prototype.hasOwnProperty
  hasOwnProperty: true,
};
// Doesn’t work in this case:
assert.throws(
  () => obj.hasOwnProperty('anyPropKey'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);
// Safe way of using .hasOwnProperty():
assert.equal(
  Object.prototype.hasOwnProperty.call(obj, 'anyPropKey'), false
);

29.9 FAQ:クラス

29.9.1 この本ではなぜ「インスタンスプライベートフィールド」と呼ばれ、「プライベートインスタンスフィールド」と呼ばれないのですか?

これは、異なるプロパティ(パブリックスロット)とプライベートスロットを強調するために行われています。形容詞の順序を変えることで、「パブリック」と「フィールド」、「プライベート」と「フィールド」という単語が常に一緒に言及されます。

29.9.2 なぜ識別子プレフィックスが # なのですか?なぜ private を使用してプライベートフィールドを宣言しないのですか?

プライベートフィールドは private を使用して宣言し、通常の識別子を使用できますか?それが可能だった場合に何が起こるかを調べてみましょう。

class MyClass {
  private value; // (A)
  compare(other) {
    return this.value === other.value;
  }
}

MyClass の本体に other.value のような式が現れると、JavaScript は次のことを決定する必要があります。

コンパイル時に、JavaScript は行Aの宣言が otherMyClass のインスタンスであるため)に適用されるかどうかを知りません。そのため、決定を行うための2つの選択肢があります。

  1. .value は常にプライベートフィールドとして解釈されます。
  2. JavaScript はランタイムで決定します。
    • otherMyClass のインスタンスである場合、.value はプライベートフィールドとして解釈されます。
    • そうでない場合、.value はプロパティとして解釈されます。

どちらの選択肢にも欠点があります。

そのため、名前プレフィックス # が導入されました。これで決定は簡単です。# を使用する場合、プライベートフィールドにアクセスしたいということです。使用しない場合、プロパティにアクセスしたいということです。

private は、静的型付け言語(TypeScript など)では機能します。コンパイル時に otherMyClass のインスタンスであるかどうかがわかっているため、.value をプライベートまたはパブリックとして扱うことができます。

  クイズ

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