instanceof
演算子.__proto__
対 .prototype
Person.prototype.constructor
(上級)this
を使用して静的プライベートフィールドにアクセスするinstanceof
とサブクラス化(上級)Object
のインスタンスであるわけではない(上級)Object.prototype
のメソッドとアクセサ(上級)Object.prototype
メソッドの安全な使用Object.prototype.toString()
Object.prototype.toLocaleString()
Object.prototype.valueOf()
Object.prototype.isPrototypeOf()
Object.prototype.propertyIsEnumerable()
Object.prototype.__proto__
(アクセサ)Object.prototype.hasOwnProperty()
本書では、JavaScriptのオブジェクト指向プログラミング(OOP)のスタイルを4つのステップで紹介します。この章ではステップ3と4を、前の章ではステップ1と2を扱います。(図12)
スーパークラス
class Person {
; // (A)
#firstNameconstructor(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');
.equal(
assert.describe(),
tarzan'Person named Tarzan'
;
).deepEqual(
assert.extractNames([tarzan, new Person('Cheeta')]),
Person'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');
.equal(
assert.title,
jane'CTO'
;
).equal(
assert.describe(),
jane'Person named Jane (CTO)'
; )
備考
.#firstName
はプライベートフィールドであり、初期化(B行)する前に宣言(A行)する必要があります。.title
はプロパティであり、事前に宣言(C行)することなく初期化できます。JavaScriptでは、インスタンスデータは(例:Javaのように隠すことを好むのではなく)比較的頻繁にパブリックになります。クラスは基本的に、プロトタイプチェーン(前の章で説明)を設定するためのコンパクトな構文です。内部的には、JavaScriptのクラスは型破りです。しかし、それらを使用する際にはめったに見ることはありません。通常、他のオブジェクト指向プログラミング言語を使用したことがある人にとってはなじみのあるものとなるはずです。
オブジェクトを作成するためにクラスは必要ありません。オブジェクトリテラルを使用して作成することもできます。そのため、シングルトンパターンはJavaScriptでは必要なく、クラスはそれを持つ他の多くの言語よりも使用頻度が低くなっています。
これまでに、人物を表す単一オブジェクトであるjane
とtarzan
を扱ってきました。そのようなオブジェクトのファクトリを実装するために、クラス宣言を使用してみましょう。
class Person {
; // (A)
#firstNameconstructor(firstName) {
this.#firstName = firstName; // (B)
}describe() {
return `Person named ${this.#firstName}`;
}static extractNames(persons) {
return persons.map(person => person.#firstName);
} }
jane
とtarzan
は、これでnew Person()
を使って作成できます。
const jane = new Person('Jane');
const tarzan = new Person('Tarzan');
クラスPerson
の本体の中身を調べてみましょう。
.constructor()
は、新しいインスタンスの作成後に呼び出される特別なメソッドです。その中で、this
はインスタンスを参照します。
[ES2022] .#firstName
はインスタンスプライベートフィールドです。このようなフィールドはインスタンスに格納されます。プロパティと同様にアクセスされますが、名前は別々です。必ずハッシュ記号(#
)で始まります。そして、クラスの外の世界からは見えません。
.deepEqual(
assertReflect.ownKeys(jane),
[]; )
コンストラクタ(B行)で.#firstName
を初期化する前に、クラス本体(A行)でそれを宣言する必要があります。
.describe()
はメソッドです。obj.describe()
を介して呼び出すと、.describe()
の本体の中ではthis
はobj
を参照します。
.equal(
assert.describe(), 'Person named Jane'
jane;
).equal(
assert.describe(), 'Person named Tarzan'
tarzan; )
.extractName()
は静的メソッドです。「静的」とは、インスタンスではなくクラスに属することを意味します。
.deepEqual(
assert.extractNames([jane, tarzan]),
Person'Jane', 'Tarzan']
[; )
コンストラクタでインスタンスプロパティ(パブリックフィールド)を作成することもできます。
class Container {
constructor(value) {
this.value = value;
}
}const abcContainer = new Container('abc');
.equal(
assert.value, 'abc'
abcContainer; )
インスタンスプライベートフィールドとは異なり、インスタンスプロパティはクラス本体で宣言する必要はありません。
クラスの定義(クラスを定義する方法)には2種類あります。
クラス式は、匿名と名前付きがあります。
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };
名前付きクラス式の名前は、名前付き関数式の名前と同様に機能します。クラスの本体内でのみアクセスでき、クラスに割り当てられているものとは関係なく同じままです。
instanceof
演算子instanceof
演算子は、値が特定のクラスのインスタンスかどうかを判断します。
> new Person('Jane') instanceof Persontrue
> {} instanceof Personfalse
> {} instanceof Objecttrue
> [] instanceof Arraytrue
サブクラス化について詳しく見てから、後でinstanceof
演算子をさらに詳しく調べます。
JavaScriptでは、オブジェクトは2種類の「スロット」を持つことができます。
プロパティとプライベートスロットについて知っておくべき最も重要なルールは以下のとおりです。
static
の使用状況やその他の要因によって異なります。 プロパティとプライベートスロットに関する詳細情報
この章では、プロパティとプライベートスロットのすべての詳細(基本的なもののみ)を網羅していません。さらに詳しく調べたい場合は、以下を参照してください。
[[PrivateElements]]
」を検索してください。次のクラスは、2種類のスロットを示しています。各インスタンスは、1つのプライベートフィールドと1つのプロパティを持っています。
class MyClass {
= 1;
#instancePrivateField = 2;
instanceProperty getInstanceValues() {
return [
this.#instancePrivateField,
this.instanceProperty,
;
]
}
}const inst = new MyClass();
.deepEqual(
assert.getInstanceValues(), [1, 2]
inst; )
予想通り、MyClass
の外側からは、プロパティしか見えません。
.deepEqual(
assertReflect.ownKeys(inst),
'instanceProperty']
[; )
次に、プライベートスロットの詳細の一部を見ていきます。
プライベートスロットは、実際にはクラスの本体の内側からしかアクセスできません。サブクラスからもアクセスできません。
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を使用した保護された可視性とフレンド可視性のシミュレーション」で説明しています。
プライベートスロットは、シンボルに似た一意のキーを持っています。以前のクラスを考えてみましょう。
class MyClass {
= 1;
#instancePrivateField = 2;
instanceProperty getInstanceValues() {
return [
this.#instancePrivateField,
this.instanceProperty,
;
]
} }
内部的には、MyClass
のプライベートフィールドは、おおよそ次のように処理されます。
let MyClass;
// Scope of the body of the class
{ const instancePrivateFieldKey = Symbol();
= class {
MyClass // Very loose approximation of how this
// works in the language specification
= new Map([
__PrivateElements__ , 1],
[instancePrivateFieldKey;
])= 2;
instanceProperty getInstanceValues() {
return [
this.__PrivateElements__.get(instancePrivateFieldKey),
this.instanceProperty,
;
]
}
} }
instancePrivateFieldKey
の値は、プライベート名と呼ばれます。プライベート名はJavaScriptでは直接使用できず、プライベートフィールド、プライベートメソッド、プライベートアクセサの固定識別子を通じて間接的にのみ使用できます。(getInstanceValues
のような)パブリックスロットの固定識別子は文字列キーとして解釈されるのに対し、(#instancePrivateField
のような)プライベートスロットの固定識別子は、プライベート名を参照します(変数名が値を参照する方法と同様です)。
プライベートスロットの識別子はキーとして使用されないため、異なるクラスで同じ識別子を使用しても、異なるスロットが生成されます(A行とC行)。
class Color {
; // (A)
#nameconstructor(name) {
this.#name = name; // (B)
}static getName(obj) {
return obj.#name;
}
}class Person {
; // (C)
#nameconstructor(name) {
this.#name = name;
}
}
.equal(
assert.getName(new Color('green')), 'green'
Color;
)
// We can’t access the private slot #name of a Person in line B:
.throws(
assert=> Color.getName(new Person('Jane')),
()
{name: 'TypeError',
message: 'Cannot read private member #name from'
+ ' an object whose class did not declare it',
}; )
サブクラスがプライベートフィールドに同じ名前を使用した場合でも、両方の名前はプライベート名(常に一意)を参照するため、衝突することはありません。次の例では、SuperClass
の.#privateField
は、両方のスロットがinst
に直接格納されている場合でも、SubClass
の.#privateField
と衝突しません。
class SuperClass {
= 'super';
#privateField getSuperPrivateField() {
return this.#privateField;
}
}class SubClass extends SuperClass {
= 'sub';
#privateField getSubPrivateField() {
return this.#privateField;
}
}const inst = new SubClass();
.equal(
assert.getSuperPrivateField(), 'super'
inst;
).equal(
assert.getSubPrivateField(), 'sub'
inst; )
extends
によるサブクラス化については、この章の後半で説明します。
in
を使用してオブジェクトに指定されたプライベートスロットがあるかどうかを確認するin
演算子を使用して、プライベートスロットが存在するかどうかを確認できます(A行)。
class Color {
;
#nameconstructor(name) {
this.#name = name;
}static check(obj) {
return #name in obj; // (A)
} }
プライベートスロットに適用されたin
のさらに多くの例を見てみましょう。
プライベートメソッド。次のコードは、プライベートメソッドがインスタンスにプライベートスロットを作成することを示しています。
class C1 {
priv() {}
#static check(obj) {
return #priv in obj;
}
}.equal(C1.check(new C1()), true); assert
静的プライベートフィールド。静的プライベートフィールドにもin
を使用できます。
class C2 {
static #priv = 1;
static check(obj) {
return #priv in obj;
}
}.equal(C2.check(C2), true);
assert.equal(C2.check(new C2()), false); assert
静的プライベートメソッド。静的プライベートメソッドのスロットも確認できます。
class C3 {
static #priv() {}
static check(obj) {
return #priv in obj;
}
}.equal(C3.check(C3), true); assert
異なるクラスで同じプライベート識別子を使用する。次の例では、Color
クラスとPerson
クラスの両方に、識別子が#name
であるスロットがあります。in
演算子はそれらを正しく区別します。
class Color {
;
#nameconstructor(name) {
this.#name = name;
}static check(obj) {
return #name in obj;
}
}class Person {
;
#nameconstructor(name) {
this.#name = name;
}static check(obj) {
return #name in obj;
}
}
// Detecting Color’s #name
.equal(
assert.check(new Color()), true
Color;
).equal(
assert.check(new Person()), false
Color;
)
// Detecting Person’s #name
.equal(
assert.check(new Person()), true
Person;
).equal(
assert.check(new Color()), false
Person; )
次の理由から、クラスの使用をお勧めします。
クラスは、オブジェクトの作成と継承のための一般的な標準であり、現在では多くのライブラリやフレームワークで広くサポートされています。これは、ほとんどすべてのフレームワークが独自の継承ライブラリを持っていた以前の方法と比較して、改善されています。
IDEや型チェッカーなどのツールが作業を支援し、新しい機能を有効にします。
他の言語からJavaScriptに移行し、クラスに慣れている場合は、より迅速に開始できます。
JavaScriptエンジンはクラスを最適化します。つまり、クラスを使用するコードは、カスタム継承ライブラリを使用するコードよりもほぼ常に高速です。
Error
などの組み込みコンストラクタ関数をサブクラス化できます。
クラスが完璧であるという意味ではありません。
継承の過剰使用のリスクがあります。
クラスに多くの機能を入れすぎるリスクがあります(その一部は多くの場合、関数の方が適しています)。
クラスは他の言語からのプログラマーにとってなじみがありますが、動作と使い方が異なります(次のサブセクションを参照)。したがって、それらのプログラマーがJavaScriptらしくないコードを書くリスクがあります。
クラスが表面的にどのように見えるかは、実際の動作とはかなり異なります。言い換えれば、構文と意味の間にはずれがあります。2つの例を挙げます。
C
内のメソッド定義は、オブジェクトC.prototype
にメソッドを作成します。ずれの動機は後方互換性です。ありがたいことに、ずれは実際にはほとんど問題を引き起こしません。クラスが装っているものに従えば、通常は問題ありません。
これはクラスの最初の概要です。すぐにさらに多くの機能を探ります。
演習:クラスの作成
exercises/classes/point_class_test.mjs
内部的には、クラスは2つの関連付けられたオブジェクトになります。クラスPerson
を再考して、それがどのように機能するかを見てみましょう。
class Person {
;
#firstNameconstructor(firstName) {
this.#firstName = firstName;
}describe() {
return `Person named ${this.#firstName}`;
}static extractNames(persons) {
return persons.map(person => person.#firstName);
} }
クラスによって作成された最初のオブジェクトは、Person
に格納されます。4つのプロパティがあります。
.deepEqual(
assertReflect.ownKeys(Person),
'length', 'name', 'prototype', 'extractNames']
[;
)
// The number of parameters of the constructor
.equal(
assert.length, 1
Person;
)
// The name of the class
.equal(
assert.name, 'Person'
Person; )
残りの2つのプロパティは次のとおりです。
Person.extractNames
は、すでに動作している静的メソッドです。Person.prototype
は、クラス定義によって作成される2番目のオブジェクトを指します。これらはPerson.prototype
の内容です。
.deepEqual(
assertReflect.ownKeys(Person.prototype),
'constructor', 'describe']
[; )
2つのプロパティがあります。
Person.prototype.constructor
はコンストラクタを指します。Person.prototype.describe
は、すでに使用しているメソッドです。オブジェクトPerson.prototype
は、すべてのインスタンスのプロトタイプです。
const jane = new Person('Jane');
.equal(
assertObject.getPrototypeOf(jane), Person.prototype
;
)
const tarzan = new Person('Tarzan');
.equal(
assertObject.getPrototypeOf(tarzan), Person.prototype
; )
これにより、インスタンスがメソッドを取得する方法が説明されます。オブジェクトPerson.prototype
から継承します。
図13は、すべてがどのように接続されているかを視覚的に示しています。
.__proto__
と.prototype
.__proto__
と.prototype
を混同しやすいです。図13は、それらがどのように異なるかを明確に示しています。
.__proto__
は、クラスObject
のアクセサであり、インスタンスのプロトタイプを取得および設定できます。
.prototype
は、他のプロパティと同じような通常のプロパティです。new
演算子がその値をインスタンスのプロトタイプとして使用する点で特別です。その名前は理想的ではありません。.instancePrototype
などの異なる名前の方が適しています。
Person.prototype.constructor
(上級)図13には、まだ見ていない詳細が1つあります。Person.prototype.constructor
はPerson
を指し返します。
> Person.prototype.constructor === Persontrue
この設定は、後方互換性のために存在します。しかし、さらに2つの利点があります。
まず、クラスの各インスタンスは.constructor
プロパティを継承します。したがって、インスタンスが与えられると、それを介して「類似した」オブジェクトを作成できます。
const jane = new Person('Jane');
const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
.equal(cheeta instanceof Person, true); assert
次に、特定のインスタンスを作成したクラスの名前を取得できます。
const tarzan = new Person('Tarzan');
.equal(tarzan.constructor.name, 'Person'); assert
このサブセクションでは、メソッドを呼び出す2つの異なる方法について学習します。
両方を理解することで、メソッドの動作に関する重要な洞察が得られます。
この章の後半でも2番目の方法が必要になります。これにより、Object.prototype
から便利なメソッドを借用できます。
クラスでのメソッド呼び出しの動作を調べてみましょう。以前のjane
を再考します。
class Person {
;
#firstNameconstructor(firstName) {
this.#firstName = firstName;
}describe() {
return 'Person named '+this.#firstName;
}
}const jane = new Person('Jane');
図14には、jane
のプロトタイプチェーンを示す図があります。
通常のメソッド呼び出しはディスパッチされます。メソッド呼び出し
.describe() jane
は2つのステップで行われます。
ディスパッチ:JavaScriptは、jane
から始まるプロトタイプチェーンを走査して、キー'describe'
を持つ独自のpropertyを持つ最初のオブジェクトを見つけます。まずjane
を見て、独自のproperty.describe
は見つかりません。jane
のプロトタイプであるPerson.prototype
に進み、値を返すdescribe
という独自のpropertyを見つけます。
const func = jane.describe;
呼び出し:メソッドを呼び出すことは、関数を呼び出すこととは異なり、括弧の前にあるものを括弧内の引数と一緒に呼び出すだけでなく、this
をメソッド呼び出しの受信者(この場合はjane
)にも設定します。
.call(jane); func
メソッドを動的に検索して呼び出すこの方法は、動的ディスパッチと呼ばれます。
ディスパッチせずに、メソッドを直接呼び出すこともできます。
.prototype.describe.call(jane) Person
今回は、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()
.throws(
assert=> obj.toString(),
() /^TypeError: obj.toString is not a function$/
;
).equal(
assertObject.prototype.toString.call(obj),
'[object Object]'
; )
ECMAScript 6より前は、JavaScriptにはクラスがありませんでした。代わりに、通常の関数がコンストラクタ関数として使用されていました。
function StringBuilderConstr(initialString) {
this.string = initialString;
}.prototype.add = function (str) {
StringBuilderConstrthis.string += str;
return this;
;
}
const sb = new StringBuilderConstr('¡');
.add('Hola').add('!');
sb.equal(
assert.string, '¡Hola!'
sb; )
クラスはこのアプローチにより優れた構文を提供します。
class StringBuilderClass {
constructor(initialString) {
this.string = initialString;
}add(str) {
this.string += str;
return this;
}
}const sb = new StringBuilderClass('¡');
.add('Hola').add('!');
sb.equal(
assert.string, '¡Hola!'
sb; )
サブクラス化は、コンストラクタ関数では特に困難です。クラスは、より便利な構文を超えた利点も提供します。
Error
などの組み込みコンストラクタ関数をサブクラス化できます。super
を介してオーバーライドされたプロパティにアクセスできます。new
で呼び出すことができず、.prototype
プロパティを持ちません。クラスはコンストラクタ関数と非常に互換性があるため、それらを拡張することさえできます。
function SuperConstructor() {}
class SubClass extends SuperConstructor {}
.equal(
assertnew SubClass() instanceof SuperConstructor, true
; )
extends
とサブクラス化については、この章の後半で説明します。
これは興味深い洞察につながります。一方では、StringBuilderClass
はStringBuilderClass.prototype.constructor
を介してコンストラクタを参照します。
一方、クラスはコンストラクタ(関数)です。
> StringBuilderClass.prototype.constructor === StringBuilderClasstrue
> typeof StringBuilderClass'function'
コンストラクタ(関数)とクラス
それらが非常に似ているため、「コンストラクタ(関数)」と「クラス」という用語を互換的に使用しています。
次のクラス宣言の本体内のすべてのメンバーは、PublicProtoClass.prototype
のプロパティを作成します。
class PublicProtoClass {
constructor(args) {
// (Do something with `args` here.)
}publicProtoMethod() {
return 'publicProtoMethod';
}publicProtoAccessor() {
get return 'publicProtoGetter';
}publicProtoAccessor(value) {
set .equal(value, 'publicProtoSetter');
assert
}
}
.deepEqual(
assertReflect.ownKeys(PublicProtoClass.prototype),
'constructor', 'publicProtoMethod', 'publicProtoAccessor']
[;
)
const inst = new PublicProtoClass('arg1', 'arg2');
.equal(
assert.publicProtoMethod(), 'publicProtoMethod'
inst;
).equal(
assert.publicProtoAccessor, 'publicProtoGetter'
inst;
).publicProtoAccessor = 'publicProtoSetter'; inst
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');
class PublicProtoClass2 {
// Identifier keys
accessor() {}
get accessor(value) {}
set syncMethod() {}
* syncGeneratorMethod() {}
async asyncMethod() {}
async * asyncGeneratorMethod() {}
// Quoted keys
'an accessor'() {}
get 'an accessor'(value) {}
set '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();
'sync method']();
inst[; inst[syncMethodKey]()
引用符付きキーと計算されたキーもオブジェクトリテラルで使用できます。
アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報
プライベートメソッド(およびアクセサ)は、プロトタイプメンバーとインスタンスメンバーの興味深い混合です。
一方、プライベートメソッドはインスタンスのスロットに格納されます(A行)。
class MyClass {
privateMethod() {}
#static check() {
const inst = new MyClass();
.equal(
assertin inst, true // (A)
#privateMethod ;
).equal(
assertin MyClass.prototype, false
#privateMethod ;
).equal(
assertin MyClass, false
#privateMethod ;
)
}
}.check(); MyClass
なぜ.prototype
オブジェクトに格納されないのですか?プライベートスロットは継承されません。プロパティだけが継承されます。
一方、プライベートメソッドは、パブリックプロトタイプメソッドのように、インスタンス間で共有されます。
class MyClass {
privateMethod() {}
#static check() {
const inst1 = new MyClass();
const inst2 = new MyClass();
.equal(
assert.#privateMethod,
inst1.#privateMethod
inst2;
)
} }
そのため、および構文がパブリックプロトタイプメソッドと似ているため、ここで説明します。
次のコードは、プライベートメソッドとアクセサの動作を示しています。
class PrivateMethodClass {
privateMethod() {
#return 'privateMethod';
}privateAccessor() {
get #return 'privateGetter';
}privateAccessor(value) {
set #.equal(value, 'privateSetter');
assert
}callPrivateMembers() {
.equal(this.#privateMethod(), 'privateMethod');
assert.equal(this.#privateAccessor, 'privateGetter');
assertthis.#privateAccessor = 'privateSetter';
}
}.deepEqual(
assertReflect.ownKeys(new PrivateMethodClass()), []
; )
プライベートスロットでは、キーは常に識別子です。
class PrivateMethodClass2 {
accessor() {}
get #accessor(value) {}
set #syncMethod() {}
#* #syncGeneratorMethod() {}
async #asyncMethod() {}
async * #asyncGeneratorMethod() {}
}
アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報
次のクラスのインスタンスには、2つのインスタンスプロパティがあります(A行とB行で作成)。
class InstPublicClass {
// Instance public field
= 0; // (A)
instancePublicField
constructor(value) {
// We don’t need to mention .property elsewhere!
this.property = value; // (B)
}
}
const inst = new InstPublicClass('constrArg');
.deepEqual(
assertReflect.ownKeys(inst),
'instancePublicField', 'property']
[;
).equal(
assert.instancePublicField, 0
inst;
).equal(
assert.property, 'constrArg'
inst; )
コンストラクタ内でインスタンスプロパティを作成する場合(B行)、他の場所で「宣言」する必要はありません。すでに見たように、インスタンスプライベートフィールドとは異なります。
JavaScriptにおけるインスタンスプロパティは比較的一般的です。例えば、Javaのようにほとんどのインスタンス状態がプライベートである場合と比較して、はるかに一般的です。
const computedFieldKey = Symbol('computedFieldKey');
class InstPublicClass2 {
'quoted field key' = 1;
= 2;
[computedFieldKey]
}const inst = new InstPublicClass2();
.equal(inst['quoted field key'], 1);
assert.equal(inst[computedFieldKey], 2); assert
インスタンスパブリックフィールドのイニシャライザにおいて、`this`は新しく作成されたインスタンスを参照します。
class MyClass {
= this;
instancePublicField
}const inst = new MyClass();
.equal(
assert.instancePublicField, inst
inst; )
インスタンスパブリックフィールドの実行は、おおよそ次の2つのルールに従います。
次の例はこれらのルールを示しています。
class SuperClass {
= console.log('superProp');
superProp constructor() {
console.log('super-constructor');
}
}class SubClass extends SuperClass {
= console.log('subProp');
subProp constructor() {
console.log('BEFORE super()');
super();
console.log('AFTER super()');
}
}new SubClass();
// Output:
// 'BEFORE super()'
// 'superProp'
// 'super-constructor'
// 'subProp'
// 'AFTER super()'
extends
とサブクラス化については、この章の後半で説明します。
次のクラスには、2つのインスタンスプライベートフィールドが含まれています(A行とB行)。
class InstPrivateClass {
= 'private field 1'; // (A)
#privateField1 ; // (B) required!
#privateField2constructor(value) {
this.#privateField2 = value; // (C)
}/**
* Private fields are not accessible outside the class body.
*/
checkPrivateValues() {
.equal(
assertthis.#privateField1, 'private field 1'
;
).equal(
assertthis.#privateField2, 'constructor argument'
;
)
}
}
const inst = new InstPrivateClass('constructor argument');
.checkPrivateValues();
inst
// No instance properties were created
.deepEqual(
assertReflect.ownKeys(inst),
[]; )
クラス本体で`.#privateField2`を宣言した場合のみ、C行でそれを使用できることに注意してください。
このセクションでは、インスタンスデータをプライベートに保持するための2つの手法を見ていきます。これらはクラスに依存しないため、オブジェクトリテラルなど、他の方法で作成されたオブジェクトにも使用できます。
最初のテクニックは、プロパティ名の前にアンダースコアを付けることでプロパティをプライベートにします。これはプロパティを何らかの方法で保護するものではありません。単に外部に対して「このプロパティを知る必要はありません」という合図を送るだけです。
次のコードでは、プロパティ`._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:
.deepEqual(
assertObject.keys(new Countdown()),
'_counter', '_action']); [
この手法では、保護機能は得られず、プライベート名が衝突する可能性があります。利点は、使い方が簡単であることです。
プライベートメソッドも同様に機能します。それらは、名前がアンダースコアで始まる通常のメソッドです。
WeakMapを使用してプライベートインスタンスデータを管理することもできます。
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
.set(this, counter);
_counter.set(this, action);
_action
}dec() {
let counter = _counter.get(this);
--;
counter.set(this, counter);
_counterif (counter === 0) {
.get(this)();
_action
}
}
}
// The two pseudo-properties are truly private:
.deepEqual(
assertObject.keys(new Countdown()),
; [])
その仕組みは、WeakMapに関する章で説明されています。
この手法は、外部からのアクセスからかなりの保護を提供し、名前の衝突もありません。しかし、使用するのもより複雑です。
擬似プロパティ`_superProp`の可視性を、誰がアクセスできるかを制御することで制御します。たとえば、変数がモジュール内に存在し、エクスポートされていない場合、モジュール内のすべての人がアクセスでき、モジュール外の誰もアクセスできません。言い換えれば、この場合のプライバシーの範囲はクラスではなくモジュールです。ただし、範囲を狭めることができます。
let Countdown;
// class scope
{ const _counter = new WeakMap();
const _action = new WeakMap();
= class {
Countdown // ···
} }
この手法は、プライベートメソッドを実際にはサポートしていません。しかし、`_superProp`にアクセスできるモジュールローカル関数は、それに次ぐ最良の方法です。
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
.set(this, counter);
_counter.set(this, action);
_action
}dec() {
privateDec(this);
}
}
function privateDec(_this) { // (A)
let counter = _counter.get(_this);
--;
counter.set(_this, counter);
_counterif (counter === 0) {
.get(_this)();
_action
} }
`this`は、明示的な関数パラメータ`_this`になります(A行)。
前述のように、インスタンスプライベートフィールドは、そのクラス内でのみ可視であり、サブクラス内でも可視ではありません。したがって、次のものを取得するための組み込みの方法はありません。
前のサブセクションでは、WeakMapを使用して「モジュール可視性」(モジュール内のすべての人がインスタンスデータの一部にアクセスできる)をシミュレートしました。したがって、
次の例は、保護された可視性を示しています。
const _superProp = new WeakMap();
class SuperClass {
constructor() {
.set(this, 'superProp');
_superProp
}
}class SubClass extends SuperClass {
getSuperProp() {
return _superProp.get(this);
}
}.equal(
assertnew SubClass().getSuperProp(),
'superProp'
; )
extends
によるサブクラス化については、この章の後半で説明します。
次のクラス宣言の本体内のすべてのメンバは、いわゆる静的プロパティを作成します。`StaticClass`自体のプロパティです。
class StaticPublicMethodsClass {
static staticMethod() {
return 'staticMethod';
}static get staticAccessor() {
return 'staticGetter';
}static set staticAccessor(value) {
.equal(value, 'staticSetter');
assert
}
}.equal(
assert.staticMethod(), 'staticMethod'
StaticPublicMethodsClass;
).equal(
assert.staticAccessor, 'staticGetter'
StaticPublicMethodsClass;
).staticAccessor = 'staticSetter'; StaticPublicMethodsClass
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:
'sync method']();
StaticPublicMethodsClass2[; StaticPublicMethodsClass2[syncMethodKey]()
引用符付きキーと計算されたキーもオブジェクトリテラルで使用できます。
アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報
次のコードは静的公開フィールドを示しています。`StaticPublicFieldClass`には3つの静的公開フィールドがあります。
const computedFieldKey = Symbol('computedFieldKey');
class StaticPublicFieldClass {
static identifierFieldKey = 1;
static 'quoted field key' = 2;
static [computedFieldKey] = 3;
}
.deepEqual(
assertReflect.ownKeys(StaticPublicFieldClass),
['length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'identifierFieldKey',
'quoted field key',
,
computedFieldKey,
];
)
.equal(StaticPublicFieldClass.identifierFieldKey, 1);
assert.equal(StaticPublicFieldClass['quoted field key'], 2);
assert.equal(StaticPublicFieldClass[computedFieldKey], 3); assert
次のクラスには、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();
}
}
.deepEqual(
assertReflect.ownKeys(StaticPrivateClass),
['length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'getResultOfTwice',
,
];
)
.equal(
assert.getResultOfTwice(),
StaticPrivateClass'hello hello'
; )
これは、すべての種類の静的プライベートスロットの完全なリストです。
class MyClass {
static #staticPrivateMethod() {}
static * #staticPrivateGeneratorMethod() {}
static async #staticPrivateAsyncMethod() {}
static async * #staticPrivateAsyncGeneratorMethod() {}
static get #staticPrivateAccessor() {}
static set #staticPrivateAccessor(value) {}
}
クラスを使用してインスタンスデータを設定するには、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つの利点があります。
静的初期化ブロックの動作に関するルールは比較的単純です。
次のコードはこれらのルールを示しています。
class SuperClass {
static superField1 = console.log('superField1');
static {
.equal(this, SuperClass);
assertconsole.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 {
.equal(this, SubClass);
assertconsole.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
によるサブクラス化については、この章の後半で説明します。
静的公開メンバでは、`this`を使用して静的公開スロットにアクセスできます。残念ながら、静的プライベートスロットにアクセスするためにそれを使用するべきではありません。
次のコードを考えてみましょう。
class SuperClass {
static publicData = 1;
static getPublicViaThis() {
return this.publicData;
}
}class SubClass extends SuperClass {
}
extends
によるサブクラス化については、この章の後半で説明します。
静的公開フィールドはプロパティです。メソッド呼び出しを行うと、
.equal(SuperClass.getPublicViaThis(), 1); assert
`this`は`SuperClass`を指し、すべてが期待通りに動作します。サブクラスを介して` .getPublicViaThis()`を呼び出すこともできます。
.equal(SubClass.getPublicViaThis(), 1); assert
`SubClass`はプロトタイプ`SuperClass`から` .getPublicViaThis()`を継承します。`this`は`SubClass`を指し、`SubClass`もプロパティ` .publicData`を継承するため、動作し続けます。
補足として、`getPublicViaThis()`で`this.publicData`に代入し、`SubClass.getPublicViaThis()`を介して呼び出した場合、`SuperClass`から継承されたプロパティを(非破壊的に)オーバーライドする`SubClass`の新しい独自の プロパティが作成されます。
次のコードを考えてみましょう。
class SuperClass {
static #privateData = 2;
static getPrivateDataViaThis() {
return this.#privateData;
}static getPrivateDataViaClassName() {
return SuperClass.#privateData;
}
}class SubClass extends SuperClass {
}
`SuperClass`を介して` .getPrivateDataViaThis()`を呼び出すと機能します。なぜなら、`this`は`SuperClass`を指しているからです。
.equal(SuperClass.getPrivateDataViaThis(), 2); assert
ただし、`SubClass`を介して` .getPrivateDataViaThis()`を呼び出すと機能しません。なぜなら、`this`は今度は`SubClass`を指し、`SubClass`には静的プライベートフィールド`.#privateData`がないからです(プロトタイプチェーンのプライベートスロットは継承されません)。
.throws(
assert=> SubClass.getPrivateDataViaThis(),
()
{name: 'TypeError',
message: 'Cannot read private member #privateData from'
+ ' an object whose class did not declare it',
}; )
回避策は、`SuperClass`を介して`.#privateData`に直接アクセスすることです。
.equal(SubClass.getPrivateDataViaClassName(), 2); assert
静的プライベートメソッドでも、同じ問題に直面します。
クラス内のすべてのメンバは、そのクラス内の他のすべてのメンバ(公開とプライベートの両方)にアクセスできます。
class DemoClass {
static #staticPrivateField = 1;
= 2;
#instPrivField
static staticMethod(inst) {
// A static method can access static private fields
// and instance private fields
.equal(DemoClass.#staticPrivateField, 1);
assert.equal(inst.#instPrivField, 2);
assert
}
protoMethod() {
// A prototype method can access instance private fields
// and static private fields
.equal(this.#instPrivField, 2);
assert.equal(DemoClass.#staticPrivateField, 1);
assert
} }
対照的に、外部からはプライベートメンバにアクセスできません。
// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
.throws(
assert=> 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).
.throws(
assert=> eval('new DemoClass().#instPrivField'),
()
{name: 'SyntaxError',
message: "Private field '#instPrivField' must"
+ " be declared in an enclosing class",
}; )
次のコードは、ハッシュ記号(`#`)を含むすべての行が原因で、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();
} }
クラスをインスタンス化する方法は複数ある場合があります。その場合、`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;
}
}
.deepEqual(
assertPoint.fromPolar(13, 0.39479111969976155),
new Point(12, 5)
; )
静的ファクトリメソッドがどれほど記述的であるかが気に入っています。`fromPolar`はインスタンスの作成方法を記述しています。JavaScriptの標準ライブラリにもそのようなファクトリメソッドがあります。たとえば、
Array.from()
Object.create()
静的ファクトリメソッドを全く持たないか、のみ静的ファクトリメソッドを持つ方が好きです。後者の場合に考慮すべき点
次のコードでは、秘密のトークン(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
.throws(
assert=> new Point(3, 4),
() TypeError
; )
クラスは既存のクラスを拡張することもできます。たとえば、次のクラス`Employee`は`Person`を拡張します。
class Person {
;
#firstNameconstructor(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');
.equal(
assert.title,
jane'CTO'
;
).equal(
assert.describe(),
jane'Person named Jane (CTO)'
; )
拡張に関連する用語
派生クラスの`.constructor()`内では、`this`にアクセスする前に、`super()`を介してスーパークラスコンストラクタを呼び出す必要があります。なぜでしょうか?
クラスのチェーンを考えてみましょう。
`new C()`を呼び出すと、`C`のコンストラクタは`B`のコンストラクタをスーパークラスとして呼び出し、`B`のコンストラクタは`A`のコンストラクタをスーパークラスとして呼び出します。インスタンスは常に基底クラスで作成され、サブクラスのコンストラクタがスロットを追加する前に作成されます。したがって、`super()`を呼び出す前にはインスタンスは存在せず、`this`を介してまだアクセスできません。
静的公開スロットは継承されることに注意してください。たとえば、`Employee`は静的メソッド`.extractNames()`を継承します。
> 'extractNames' in Employeetrue
演習:サブクラス化
exercises/classes/color_point_class_test.mjs
前のセクションのクラス`Person`と`Employee`は、いくつかのオブジェクトで構成されています(図15)。これらのオブジェクトの関係を理解するための重要な洞察の1つは、2つのプロトタイプチェーンが存在することです。
インスタンスのプロトタイプチェーンはjane
から始まり、Employee.prototype
、Person.prototype
と続きます。原則として、プロトタイプチェーンはここで終わりますが、もう一つオブジェクトObject.prototype
が追加されます。このプロトタイプは事実上すべてのオブジェクトにサービスを提供するため、ここにも含まれています。
> Object.getPrototypeOf(Person.prototype) === Object.prototypetrue
クラスのプロトタイプチェーンでは、最初にEmployee
、次にPerson
がきます。その後、チェーンはFunction.prototype
と続きます。これは、Person
が関数であり、関数はFunction.prototype
のサービスを必要とするためです。
> Object.getPrototypeOf(Person) === Function.prototypetrue
instanceof
とサブクラス化(上級)instanceof
が実際にどのように動作するかはまだ学習していません。instanceof
は、値x
がクラスC
のインスタンスであるかどうか(C
の直接インスタンス、またはC
のサブクラスの直接インスタンスのいずれか)をどのように判断しますか?それは、C.prototype
がx
のプロトタイプチェーン内にあるかどうかを確認します。つまり、次の2つの式は同等です。
instanceof C
x .prototype.isPrototypeOf(x) C
図15に戻ると、プロトタイプチェーンが次の正しい答えに導くことを確認できます。
> jane instanceof Employeetrue
> jane instanceof Persontrue
> jane instanceof Objecttrue
instanceof
は、左辺がプリミティブ値の場合、常にfalse
を返すことに注意してください。
> 'abc' instanceof Stringfalse
> 123 instanceof Numberfalse
Object
のインスタンスであるわけではない(上級)オブジェクト(プリミティブではない値)は、Object.prototype
がそのプロトタイプチェーン内にある場合にのみ、Object
のインスタンスです (前のセクションを参照)。事実上すべてのオブジェクトはObject
のインスタンスです。例えば
.equal(
asserta: 1} instanceof Object, true
{;
).equal(
assert'a'] instanceof Object, true
[;
).equal(
assert/abc/g instanceof Object, true
;
).equal(
assertnew Map() instanceof Object, true
;
)
class C {}
.equal(
assertnew C() instanceof Object, true
; )
次の例では、obj1
とobj2
はどちらもオブジェクトです(A行とC行)。しかし、それらはObject
のインスタンスではありません(B行とD行)。Object.prototype
は、プロトタイプを持たないため、それらのプロトタイプチェーンにはありません。
const obj1 = {__proto__: null};
.equal(
asserttypeof obj1, 'object' // (A)
;
).equal(
assertinstanceof Object, false // (B)
obj1 ;
)
const obj2 = Object.create(null);
.equal(
asserttypeof obj2, 'object' // (C)
;
).equal(
assertinstanceof Object, false // (D)
obj2 ; )
Object.prototype
は、ほとんどのプロトタイプチェーンを終了するオブジェクトです。そのプロトタイプはnull
であるため、それ自体もObject
のインスタンスではありません。
> typeof Object.prototype'object'
> Object.getPrototypeOf(Object.prototype)null
> Object.prototype instanceof Objectfalse
次に、サブクラス化の知識を使用して、いくつかの組み込みオブジェクトのプロトタイプチェーンを理解します。次のツール関数p()
は、調査に役立ちます。
const p = Object.getPrototypeOf.bind(Object);
Object
の.getPrototypeOf()
メソッドを抽出し、p
に代入しました。
{}
のプロトタイプチェーンまず、プレーンオブジェクトを調べましょう。
> p({}) === Object.prototypetrue
> p(p({})) === nulltrue
図16はこのプロトタイプチェーンの図を示しています。{}
が実際にObject
のインスタンスであることがわかります。Object.prototype
はそのプロトタイプチェーンにあります。
[]
のプロトタイプチェーン配列のプロトタイプチェーンはどうなっていますか?
> p([]) === Array.prototypetrue
> p(p([])) === Object.prototypetrue
> p(p(p([]))) === nulltrue
このプロトタイプチェーン(図17に可視化されています)は、配列オブジェクトがArray
とObject
のインスタンスであることを示しています。
function () {}
のプロトタイプチェーン最後に、通常の関数のプロトタイプチェーンは、すべての関数がオブジェクトであることを示しています。
> p(function () {}) === Function.prototypetrue
> p(p(function () {})) === Object.prototypetrue
基底クラスのプロトタイプはFunction.prototype
であるため、それは関数(Function
のインスタンス)です。
class A {}
.equal(
assertObject.getPrototypeOf(A),
Function.prototype
;
)
.equal(
assertObject.getPrototypeOf(class {}),
Function.prototype
; )
派生クラスのプロトタイプは、そのスーパークラスです。
class B extends A {}
.equal(
assertObject.getPrototypeOf(B),
A;
)
.equal(
assertObject.getPrototypeOf(class extends Object {}),
Object
; )
興味深いことに、Object
、Array
、Function
はすべて基底クラスです。
> Object.getPrototypeOf(Object) === Function.prototypetrue
> Object.getPrototypeOf(Array) === Function.prototypetrue
> Object.getPrototypeOf(Function) === Function.prototypetrue
しかし、見てきたように、基底クラスのインスタンスでさえ、すべてのオブジェクトに必要なサービスを提供するため、プロトタイプチェーンにObject.prototype
が含まれています。
なぜ
Array
とFunction
は基底クラスなのですか?
基底クラスは、インスタンスが実際に作成される場所です。Array
とFunction
はどちらも、後でObject
によって作成されたインスタンスに追加できない「内部スロット」と呼ばれるものを持っているため、独自のインスタンスを作成する必要があります。
JavaScriptのクラスシステムは、単一継承のみをサポートしています。つまり、各クラスは最大1つのスーパークラスを持つことができます。この制限を回避する1つの方法は、ミキシンクラス(略してミキシン)と呼ばれる手法です。
考え方は次のとおりです。クラスC
が2つのスーパークラスS1
とS2
から継承したいとします。それは多重継承になりますが、JavaScriptはサポートしていません。
回避策として、S1
とS2
をサブクラスのファクトリであるミキシンに変えます。
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
ができました。
オブジェクトのブランドの設定と取得のためのヘルパーメソッドを持つミキシンBranded
を実装します。
const Named = (Sup) => class extends Sup {
= '(Unnamed)';
name 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');
.equal(
assert.name, 'Paris'
paris;
).equal(
assert.toString(), 'City named Paris'
paris; )
ミキシンは、単一継承の制約から解放します。
Object.prototype
のメソッドとアクセサ(上級)§29.7.3 「すべてのオブジェクトがObject
のインスタンスであるわけではない」で見たように、ほとんどすべてのオブジェクトはObject
のインスタンスです。このクラスは、インスタンスにいくつかの便利なメソッドとアクセサを提供します。
+
演算子による):次のメソッドにはデフォルトの実装がありますが、サブクラスまたはインスタンスでオーバーライドされることがよくあります。.toString()
:オブジェクトが文字列に変換される方法を設定します。.toLocaleString()
:引数(言語、地域など)によってさまざまな方法で設定できる.toString()
のバージョンです。.valueOf()
:オブジェクトが文字列以外のプリミティブ値(多くの場合、数値)に変換される方法を設定します。.isPrototypeOf()
:レシーバが指定されたオブジェクトのプロトタイプチェーンにありますか?.propertyIsEnumerable()
:レシーバは、指定されたキーを持つ列挙可能な独自の特性を持っていますか?.__proto__
:レシーバのプロトタイプを取得および設定します。Object.getPrototypeOf()
Object.setPrototypeOf()
.hasOwnProperty()
:レシーバは、指定されたキーを持つ独自の特性を持っていますか?Object.hasOwn()
。これらの機能を詳しく見ていく前に、重要な落とし穴(とその回避方法)について学習します。Object.prototype
の機能をすべてのオブジェクトで使用することはできません。
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);
.equal(obj instanceof Object, false);
assert.throws(
assert=> obj.hasOwnProperty('prop'),
()
{name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}; )
もう一方、オブジェクトが独自の特性でそれをオーバーライドする場合(A行)、.hasOwnProperty()
を使用できません。
const obj = {
hasOwnProperty: 'yes' // (A)
;
}.throws(
assert=> 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)
}.equal(
asserthasOwnProp(Object.create(null), 'prop'), false
;
).equal(
asserthasOwnProp({hasOwnProperty: 'yes'}, 'prop'), false
;
).equal(
asserthasOwnProp({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()
なしで)呼び出すことができます。レシーバがわかっていて、固定レイアウトオブジェクトである場合です。
一方、レシーバがわからない場合や、辞書オブジェクトである場合は、注意が必要です。
Object.prototype.toString()
.toString()
(サブクラスまたはインスタンスで)をオーバーライドすることにより、オブジェクトが文字列に変換される方法を設定できます。
> String({toString() { return 'Hello!' }})'Hello!'
> String({})'[object Object]'
オブジェクトを文字列に変換するには、String()
を使用する方が良いでしょう。これはundefined
とnull
でも機能するからです。
> 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'
Object.prototype.toLocaleString()
.toLocaleString()
は、ロケールと多くの追加オプションで設定できる.toString()
のバージョンです。任意のクラスまたはインスタンスがこのメソッドを実装できます。標準ライブラリでは、次のクラスが実装しています。
Array.prototype.toLocaleString()
Number.prototype.toLocaleString()
Date.prototype.toLocaleString()
TypedArray.prototype.toLocaleString()
BigInt.prototype.toLocaleString()
例として、小数を含む数値は、ロケール('fr'
はフランス語、'en'
は英語)によって異なる文字列に変換されます。
> 123.45.toLocaleString('fr')'123,45'
> 123.45.toLocaleString('en')'123.45'
Object.prototype.valueOf()
.valueOf()
(サブクラスまたはインスタンスで)をオーバーライドすることにより、オブジェクトが文字列以外の値(多くの場合、数値)に変換される方法を設定できます。
> Number({valueOf() { return 123 }})123
> Number({})NaN
Object.prototype.isPrototypeOf()
proto.isPrototypeOf(obj)
は、proto
が obj
のプロトタイプチェーンに存在する場合は true
を、そうでない場合は false
を返します。
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert
.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false); assert
このメソッドを安全に使用する方法は次のとおりです(詳細については §29.8.1「Object.prototype
メソッドの安全な使用方法」 を参照してください)。
const obj = {
// Overrides Object.prototype.isPrototypeOf
isPrototypeOf: true,
;
}// Doesn’t work in this case:
.throws(
assert=> obj.isPrototypeOf(Object.prototype),
()
{name: 'TypeError',
message: 'obj.isPrototypeOf is not a function',
};
)// Safe way of using .isPrototypeOf():
.equal(
assertObject.prototype.isPrototypeOf.call(obj, Object.prototype), false
; )
Object.prototype.propertyIsEnumerable()
obj.propertyIsEnumerable(propKey)
は、obj
がキーが propKey
である列挙可能な独自のプロパティを持つ場合は true
を、そうでない場合は false
を返します。
const proto = {
enumerableProtoProp: true,
;
}const obj = {
__proto__: proto,
enumerableObjProp: true,
nonEnumObjProp: true,
;
}Object.defineProperty(
, 'nonEnumObjProp',
obj
{enumerable: false,
};
)
.equal(
assert.propertyIsEnumerable('enumerableProtoProp'),
objfalse // not an own property
;
).equal(
assert.propertyIsEnumerable('enumerableObjProp'),
objtrue
;
).equal(
assert.propertyIsEnumerable('nonEnumObjProp'),
objfalse // not enumerable
;
).equal(
assert.propertyIsEnumerable('unknownProp'),
objfalse // not a property
; )
このメソッドを安全に使用する方法は次のとおりです(詳細については §29.8.1「Object.prototype
メソッドの安全な使用方法」 を参照してください)。
const obj = {
// Overrides Object.prototype.propertyIsEnumerable
propertyIsEnumerable: true,
enumerableProp: 'yes',
;
}// Doesn’t work in this case:
.throws(
assert=> obj.propertyIsEnumerable('enumerableProp'),
()
{name: 'TypeError',
message: 'obj.propertyIsEnumerable is not a function',
};
)// Safe way of using .propertyIsEnumerable():
.equal(
assertObject.prototype.propertyIsEnumerable.call(obj, 'enumerableProp'),
true
; )
別の安全な代替手段として、プロパティ記述子 を使用する方法があります。
.deepEqual(
assertObject.getOwnPropertyDescriptor(obj, 'enumerableProp'),
{value: 'yes',
writable: true,
enumerable: true,
configurable: true,
}; )
Object.prototype.__proto__
(アクセサ)__proto__
プロパティには2つのバージョンがあります。
Object
のインスタンスが持つアクセサ。前者の機能の使用は避けることをお勧めします。
Object.prototype
メソッドの安全な使用方法」で説明されているように、すべてのオブジェクトで動作するわけではありません。一方、オブジェクトリテラル内の __proto__
は常に動作し、非推奨ではありません。
アクセサ __proto__
の動作に興味がある場合は、読み進めてください。
__proto__
は Object.prototype
のアクセサであり、すべての Object
のインスタンスによって継承されます。クラスを使用して実装すると次のようになります。
class Object {
__proto__() {
get return Object.getPrototypeOf(this);
}__proto__(other) {
set Object.setPrototypeOf(this, other);
}// ···
}
__proto__
は Object.prototype
から継承されるため、プロトタイプチェーンに Object.prototype
を持たないオブジェクトを作成することで、この機能を削除できます(§29.7.3「すべてのオブジェクトが Object
のインスタンスであるとは限らない」を参照)。
> '__proto__' in {}true
> '__proto__' in Object.create(null)false
Object.prototype.hasOwnProperty()
.hasOwnProperty()
のより良い代替手段:Object.hasOwn()
[ES2022]
§28.9.4「Object.hasOwn()
:指定されたプロパティが独自の(非継承)プロパティかどうかを調べます [ES2022]」を参照してください。
obj.hasOwnProperty(propKey)
は、obj
がキーが propKey
である独自の(非継承)プロパティを持つ場合は true
を、そうでない場合は false
を返します。
const obj = { ownProp: true };
.equal(
assert.hasOwnProperty('ownProp'), true // own
obj;
).equal(
assert'toString' in obj, true // inherited
;
).equal(
assert.hasOwnProperty('toString'), false
obj; )
このメソッドを安全に使用する方法は次のとおりです(詳細については §29.8.1「Object.prototype
メソッドの安全な使用方法」 を参照してください)。
const obj = {
// Overrides Object.prototype.hasOwnProperty
hasOwnProperty: true,
;
}// Doesn’t work in this case:
.throws(
assert=> obj.hasOwnProperty('anyPropKey'),
()
{name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
};
)// Safe way of using .hasOwnProperty():
.equal(
assertObject.prototype.hasOwnProperty.call(obj, 'anyPropKey'), false
; )
これは、異なるプロパティ(パブリックスロット)とプライベートスロットを強調するために行われています。形容詞の順序を変えることで、「パブリック」と「フィールド」、「プライベート」と「フィールド」という単語が常に一緒に言及されます。
#
なのですか?なぜ private
を使用してプライベートフィールドを宣言しないのですか?プライベートフィールドは private
を使用して宣言し、通常の識別子を使用できますか?それが可能だった場合に何が起こるかを調べてみましょう。
class MyClass {
private value; // (A)
compare(other) {
return this.value === other.value;
} }
MyClass
の本体に other.value
のような式が現れると、JavaScript は次のことを決定する必要があります。
.value
はプロパティですか?.value
はプライベートフィールドですか?コンパイル時に、JavaScript は行Aの宣言が other
(MyClass
のインスタンスであるため)に適用されるかどうかを知りません。そのため、決定を行うための2つの選択肢があります。
.value
は常にプライベートフィールドとして解釈されます。other
が MyClass
のインスタンスである場合、.value
はプライベートフィールドとして解釈されます。.value
はプロパティとして解釈されます。どちらの選択肢にも欠点があります。
.value
をプロパティとして使用できなくなります(どのオブジェクトでも)。そのため、名前プレフィックス #
が導入されました。これで決定は簡単です。#
を使用する場合、プライベートフィールドにアクセスしたいということです。使用しない場合、プロパティにアクセスしたいということです。
private
は、静的型付け言語(TypeScript など)では機能します。コンパイル時に other
が MyClass
のインスタンスであるかどうかがわかっているため、.value
をプライベートまたはパブリックとして扱うことができます。
クイズ
クイズアプリを参照してください。