instanceof 演算子.__proto__ 対 .prototypePerson.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 {
#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)'
);備考
.#firstNameはプライベートフィールドであり、初期化(B行)する前に宣言(A行)する必要があります。.titleはプロパティであり、事前に宣言(C行)することなく初期化できます。JavaScriptでは、インスタンスデータは(例:Javaのように隠すことを好むのではなく)比較的頻繁にパブリックになります。クラスは基本的に、プロトタイプチェーン(前の章で説明)を設定するためのコンパクトな構文です。内部的には、JavaScriptのクラスは型破りです。しかし、それらを使用する際にはめったに見ることはありません。通常、他のオブジェクト指向プログラミング言語を使用したことがある人にとってはなじみのあるものとなるはずです。
オブジェクトを作成するためにクラスは必要ありません。オブジェクトリテラルを使用して作成することもできます。そのため、シングルトンパターンはJavaScriptでは必要なく、クラスはそれを持つ他の多くの言語よりも使用頻度が低くなっています。
これまでに、人物を表す単一オブジェクトであるjaneとtarzanを扱ってきました。そのようなオブジェクトのファクトリを実装するために、クラス宣言を使用してみましょう。
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);
}
}janeとtarzanは、これでnew Person()を使って作成できます。
const jane = new Person('Jane');
const tarzan = new Person('Tarzan');クラスPersonの本体の中身を調べてみましょう。
.constructor()は、新しいインスタンスの作成後に呼び出される特別なメソッドです。その中で、thisはインスタンスを参照します。
[ES2022] .#firstNameはインスタンスプライベートフィールドです。このようなフィールドはインスタンスに格納されます。プロパティと同様にアクセスされますが、名前は別々です。必ずハッシュ記号(#)で始まります。そして、クラスの外の世界からは見えません。
assert.deepEqual(
Reflect.ownKeys(jane),
[]
);コンストラクタ(B行)で.#firstNameを初期化する前に、クラス本体(A行)でそれを宣言する必要があります。
.describe()はメソッドです。obj.describe()を介して呼び出すと、.describe()の本体の中ではthisはobjを参照します。
assert.equal(
jane.describe(), 'Person named Jane'
);
assert.equal(
tarzan.describe(), 'Person named Tarzan'
);.extractName()は静的メソッドです。「静的」とは、インスタンスではなくクラスに属することを意味します。
assert.deepEqual(
Person.extractNames([jane, tarzan]),
['Jane', 'Tarzan']
);コンストラクタでインスタンスプロパティ(パブリックフィールド)を作成することもできます。
class Container {
constructor(value) {
this.value = value;
}
}
const abcContainer = new Container('abc');
assert.equal(
abcContainer.value, 'abc'
);インスタンスプライベートフィールドとは異なり、インスタンスプロパティはクラス本体で宣言する必要はありません。
クラスの定義(クラスを定義する方法)には2種類あります。
クラス式は、匿名と名前付きがあります。
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };名前付きクラス式の名前は、名前付き関数式の名前と同様に機能します。クラスの本体内でのみアクセスでき、クラスに割り当てられているものとは関係なく同じままです。
instanceof 演算子instanceof演算子は、値が特定のクラスのインスタンスかどうかを判断します。
> new Person('Jane') instanceof Person
true
> {} instanceof Person
false
> {} instanceof Object
true
> [] instanceof Array
trueサブクラス化について詳しく見てから、後でinstanceof演算子をさらに詳しく調べます。
JavaScriptでは、オブジェクトは2種類の「スロット」を持つことができます。
プロパティとプライベートスロットについて知っておくべき最も重要なルールは以下のとおりです。
staticの使用状況やその他の要因によって異なります。 プロパティとプライベートスロットに関する詳細情報
この章では、プロパティとプライベートスロットのすべての詳細(基本的なもののみ)を網羅していません。さらに詳しく調べたい場合は、以下を参照してください。
[[PrivateElements]]」を検索してください。次のクラスは、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']
);次に、プライベートスロットの詳細の一部を見ていきます。
プライベートスロットは、実際にはクラスの本体の内側からしかアクセスできません。サブクラスからもアクセスできません。
class SuperClass {
#superProp = 'superProp';
}
class SubClass extends SuperClass {
getSuperProp() {
return this.#superProp;
}
}
// SyntaxError: Private field '#superProp'
// must be declared in an enclosing classextendsによるサブクラス化については、この章の後半で説明します。この制限を回避する方法については、§29.5.4「WeakMapを使用した保護された可視性とフレンド可視性のシミュレーション」で説明しています。
プライベートスロットは、シンボルに似た一意のキーを持っています。以前のクラスを考えてみましょう。
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のような)プライベートスロットの固定識別子は、プライベート名を参照します(変数名が値を参照する方法と同様です)。
プライベートスロットの識別子はキーとして使用されないため、異なるクラスで同じ識別子を使用しても、異なるスロットが生成されます(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',
}
);サブクラスがプライベートフィールドに同じ名前を使用した場合でも、両方の名前はプライベート名(常に一意)を参照するため、衝突することはありません。次の例では、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によるサブクラス化については、この章の後半で説明します。
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
);次の理由から、クラスの使用をお勧めします。
クラスは、オブジェクトの作成と継承のための一般的な標準であり、現在では多くのライブラリやフレームワークで広くサポートされています。これは、ほとんどすべてのフレームワークが独自の継承ライブラリを持っていた以前の方法と比較して、改善されています。
IDEや型チェッカーなどのツールが作業を支援し、新しい機能を有効にします。
他の言語からJavaScriptに移行し、クラスに慣れている場合は、より迅速に開始できます。
JavaScriptエンジンはクラスを最適化します。つまり、クラスを使用するコードは、カスタム継承ライブラリを使用するコードよりもほぼ常に高速です。
Errorなどの組み込みコンストラクタ関数をサブクラス化できます。
クラスが完璧であるという意味ではありません。
継承の過剰使用のリスクがあります。
クラスに多くの機能を入れすぎるリスクがあります(その一部は多くの場合、関数の方が適しています)。
クラスは他の言語からのプログラマーにとってなじみがありますが、動作と使い方が異なります(次のサブセクションを参照)。したがって、それらのプログラマーがJavaScriptらしくないコードを書くリスクがあります。
クラスが表面的にどのように見えるかは、実際の動作とはかなり異なります。言い換えれば、構文と意味の間にはずれがあります。2つの例を挙げます。
C内のメソッド定義は、オブジェクトC.prototypeにメソッドを作成します。ずれの動機は後方互換性です。ありがたいことに、ずれは実際にはほとんど問題を引き起こしません。クラスが装っているものに従えば、通常は問題ありません。
これはクラスの最初の概要です。すぐにさらに多くの機能を探ります。
演習:クラスの作成
exercises/classes/point_class_test.mjs
内部的には、クラスは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.extractNamesは、すでに動作している静的メソッドです。Person.prototypeは、クラス定義によって作成される2番目のオブジェクトを指します。これらはPerson.prototypeの内容です。
assert.deepEqual(
Reflect.ownKeys(Person.prototype),
['constructor', 'describe']
);2つのプロパティがあります。
Person.prototype.constructorはコンストラクタを指します。Person.prototype.describeは、すでに使用しているメソッドです。オブジェクト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は、すべてがどのように接続されているかを視覚的に示しています。
.__proto__と.prototype.__proto__と.prototypeを混同しやすいです。図13は、それらがどのように異なるかを明確に示しています。
.__proto__は、クラスObjectのアクセサであり、インスタンスのプロトタイプを取得および設定できます。
.prototypeは、他のプロパティと同じような通常のプロパティです。new演算子がその値をインスタンスのプロトタイプとして使用する点で特別です。その名前は理想的ではありません。.instancePrototypeなどの異なる名前の方が適しています。
Person.prototype.constructor(上級)図13には、まだ見ていない詳細が1つあります。Person.prototype.constructorはPersonを指し返します。
> 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');このサブセクションでは、メソッドを呼び出す2つの異なる方法について学習します。
両方を理解することで、メソッドの動作に関する重要な洞察が得られます。
この章の後半でも2番目の方法が必要になります。これにより、Object.prototypeから便利なメソッドを借用できます。
クラスでのメソッド呼び出しの動作を調べてみましょう。以前のjaneを再考します。
class Person {
#firstName;
constructor(firstName) {
this.#firstName = firstName;
}
describe() {
return 'Person named '+this.#firstName;
}
}
const jane = new Person('Jane');図14には、janeのプロトタイプチェーンを示す図があります。
通常のメソッド呼び出しはディスパッチされます。メソッド呼び出し
jane.describe()は2つのステップで行われます。
ディスパッチ:JavaScriptは、janeから始まるプロトタイプチェーンを走査して、キー'describe'を持つ独自のpropertyを持つ最初のオブジェクトを見つけます。まずjaneを見て、独自のproperty.describeは見つかりません。janeのプロトタイプであるPerson.prototypeに進み、値を返すdescribeという独自のpropertyを見つけます。
const func = jane.describe;呼び出し:メソッドを呼び出すことは、関数を呼び出すこととは異なり、括弧の前にあるものを括弧内の引数と一緒に呼び出すだけでなく、thisをメソッド呼び出しの受信者(この場合はjane)にも設定します。
func.call(jane);メソッドを動的に検索して呼び出すこの方法は、動的ディスパッチと呼ばれます。
ディスパッチせずに、メソッドを直接呼び出すこともできます。
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]'
);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!'
);サブクラス化は、コンストラクタ関数では特に困難です。クラスは、より便利な構文を超えた利点も提供します。
Errorなどの組み込みコンストラクタ関数をサブクラス化できます。superを介してオーバーライドされたプロパティにアクセスできます。newで呼び出すことができず、.prototypeプロパティを持ちません。クラスはコンストラクタ関数と非常に互換性があるため、それらを拡張することさえできます。
function SuperConstructor() {}
class SubClass extends SuperConstructor {}
assert.equal(
new SubClass() instanceof SuperConstructor, true
);extendsとサブクラス化については、この章の後半で説明します。
これは興味深い洞察につながります。一方では、StringBuilderClassはStringBuilderClass.prototype.constructorを介してコンストラクタを参照します。
一方、クラスはコンストラクタ(関数)です。
> StringBuilderClass.prototype.constructor === StringBuilderClass
true
> typeof StringBuilderClass
'function' コンストラクタ(関数)とクラス
それらが非常に似ているため、「コンストラクタ(関数)」と「クラス」という用語を互換的に使用しています。
次のクラス宣言の本体内のすべてのメンバーは、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';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]();引用符付きキーと計算されたキーもオブジェクトリテラルで使用できます。
アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報
プライベートメソッド(およびアクセサ)は、プロトタイプメンバーとインスタンスメンバーの興味深い混合です。
一方、プライベートメソッドはインスタンスのスロットに格納されます(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()), []
);プライベートスロットでは、キーは常に識別子です。
class PrivateMethodClass2 {
get #accessor() {}
set #accessor(value) {}
#syncMethod() {}
* #syncGeneratorMethod() {}
async #asyncMethod() {}
async * #asyncGeneratorMethod() {}
}アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報
次のクラスのインスタンスには、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のようにほとんどのインスタンス状態がプライベートである場合と比較して、はるかに一般的です。
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);インスタンスパブリックフィールドのイニシャライザにおいて、`this`は新しく作成されたインスタンスを参照します。
class MyClass {
instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
inst.instancePublicField, inst
);インスタンスパブリックフィールドの実行は、おおよそ次の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とサブクラス化については、この章の後半で説明します。
次のクラスには、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行でそれを使用できることに注意してください。
このセクションでは、インスタンスデータをプライベートに保持するための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:
assert.deepEqual(
Object.keys(new Countdown()),
['_counter', '_action']);この手法では、保護機能は得られず、プライベート名が衝突する可能性があります。利点は、使い方が簡単であることです。
プライベートメソッドも同様に機能します。それらは、名前がアンダースコアで始まる通常のメソッドです。
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行)。
前述のように、インスタンスプライベートフィールドは、そのクラス内でのみ可視であり、サブクラス内でも可視ではありません。したがって、次のものを取得するための組み込みの方法はありません。
前のサブセクションでは、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によるサブクラス化については、この章の後半で説明します。
次のクラス宣言の本体内のすべてのメンバは、いわゆる静的プロパティを作成します。`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';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]();引用符付きキーと計算されたキーもオブジェクトリテラルで使用できます。
アクセサ(ゲッターおよび/またはセッターで定義)、ジェネレータ、非同期メソッド、非同期ジェネレータメソッドの詳細情報
次のコードは静的公開フィールドを示しています。`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);次のクラスには、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) {}
}クラスを使用してインスタンスデータを設定するには、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 {
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によるサブクラス化については、この章の後半で説明します。
静的公開メンバでは、`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`の新しい独自の プロパティが作成されます。
次のコードを考えてみましょう。
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);静的プライベートメソッドでも、同じ問題に直面します。
クラス内のすべてのメンバは、そのクラス内の他のすべてのメンバ(公開とプライベートの両方)にアクセスできます。
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",
}
);次のコードは、ハッシュ記号(`#`)を含むすべての行が原因で、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;
}
}
assert.deepEqual(
Point.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
assert.throws(
() => new Point(3, 4),
TypeError
);クラスは既存のクラスを拡張することもできます。たとえば、次のクラス`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
前のセクションのクラス`Person`と`Employee`は、いくつかのオブジェクトで構成されています(図15)。これらのオブジェクトの関係を理解するための重要な洞察の1つは、2つのプロトタイプチェーンが存在することです。
インスタンスのプロトタイプチェーンはjaneから始まり、Employee.prototype、Person.prototypeと続きます。原則として、プロトタイプチェーンはここで終わりますが、もう一つオブジェクトObject.prototypeが追加されます。このプロトタイプは事実上すべてのオブジェクトにサービスを提供するため、ここにも含まれています。
> Object.getPrototypeOf(Person.prototype) === Object.prototype
trueクラスのプロトタイプチェーンでは、最初にEmployee、次にPersonがきます。その後、チェーンはFunction.prototypeと続きます。これは、Personが関数であり、関数はFunction.prototypeのサービスを必要とするためです。
> Object.getPrototypeOf(Person) === Function.prototype
trueinstanceofとサブクラス化(上級)instanceofが実際にどのように動作するかはまだ学習していません。instanceofは、値xがクラスCのインスタンスであるかどうか(Cの直接インスタンス、またはCのサブクラスの直接インスタンスのいずれか)をどのように判断しますか?それは、C.prototypeがxのプロトタイプチェーン内にあるかどうかを確認します。つまり、次の2つの式は同等です。
x instanceof C
C.prototype.isPrototypeOf(x)図15に戻ると、プロトタイプチェーンが次の正しい答えに導くことを確認できます。
> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
trueinstanceofは、左辺がプリミティブ値の場合、常にfalseを返すことに注意してください。
> 'abc' instanceof String
false
> 123 instanceof Number
falseObjectのインスタンスであるわけではない(上級)オブジェクト(プリミティブではない値)は、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
);次の例では、obj1とobj2はどちらもオブジェクトです(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次に、サブクラス化の知識を使用して、いくつかの組み込みオブジェクトのプロトタイプチェーンを理解します。次のツール関数p()は、調査に役立ちます。
const p = Object.getPrototypeOf.bind(Object);Objectの.getPrototypeOf()メソッドを抽出し、pに代入しました。
{}のプロトタイプチェーンまず、プレーンオブジェクトを調べましょう。
> p({}) === Object.prototype
true
> p(p({})) === null
true図16はこのプロトタイプチェーンの図を示しています。{}が実際にObjectのインスタンスであることがわかります。Object.prototypeはそのプロトタイプチェーンにあります。
[]のプロトタイプチェーン配列のプロトタイプチェーンはどうなっていますか?
> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
trueこのプロトタイプチェーン(図17に可視化されています)は、配列オブジェクトがArrayとObjectのインスタンスであることを示しています。
function () {}のプロトタイプチェーン最後に、通常の関数のプロトタイプチェーンは、すべての関数がオブジェクトであることを示しています。
> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true基底クラスのプロトタイプは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
);興味深いことに、Object、Array、Functionはすべて基底クラスです。
> Object.getPrototypeOf(Object) === Function.prototype
true
> Object.getPrototypeOf(Array) === Function.prototype
true
> Object.getPrototypeOf(Function) === Function.prototype
trueしかし、見てきたように、基底クラスのインスタンスでさえ、すべてのオブジェクトに必要なサービスを提供するため、プロトタイプチェーンに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 {
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'
);ミキシンは、単一継承の制約から解放します。
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);
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()なしで)呼び出すことができます。レシーバがわかっていて、固定レイアウトオブジェクトである場合です。
一方、レシーバがわからない場合や、辞書オブジェクトである場合は、注意が必要です。
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({})
NaNObject.prototype.isPrototypeOf()proto.isPrototypeOf(obj) は、proto が obj のプロトタイプチェーンに存在する場合は 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
);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,
}
);Object.prototype.__proto__(アクセサ)__proto__ プロパティには2つのバージョンがあります。
Object のインスタンスが持つアクセサ。前者の機能の使用は避けることをお勧めします。
Object.prototype メソッドの安全な使用方法」で説明されているように、すべてのオブジェクトで動作するわけではありません。一方、オブジェクトリテラル内の __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)
falseObject.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
);これは、異なるプロパティ(パブリックスロット)とプライベートスロットを強調するために行われています。形容詞の順序を変えることで、「パブリック」と「フィールド」、「プライベート」と「フィールド」という単語が常に一緒に言及されます。
# なのですか?なぜ 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 をプライベートまたはパブリックとして扱うことができます。
クイズ
クイズアプリを参照してください。