instanceof 演算子instanceof の詳細 (高度)本書では、JavaScript のオブジェクト指向プログラミング (OOP) のスタイルを 4 つのステップで紹介します。この章ではステップ 2~4 を扱い、前の章ではステップ 1 を扱っています。ステップは次のとおりです (図 9)。
プロトタイプは、JavaScript の唯一の継承メカニズムです。各オブジェクトには、null またはオブジェクトであるプロトタイプがあります。後者の場合、オブジェクトはプロトタイプのすべてのプロパティを継承します。
オブジェクトリテラルでは、特別なプロパティ __proto__ を使用してプロトタイプを設定できます。
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
// obj inherits .protoProp:
assert.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true);プロトタイプオブジェクト自体がプロトタイプを持つことができるため、オブジェクトのチェーン (いわゆるプロトタイプチェーン) が作成されます。つまり、継承によって単一のオブジェクトを扱っているように見えますが、実際にはオブジェクトのチェーンを扱っています。
図 10 は、obj のプロトタイプチェーンがどのように見えるかを示しています。
継承されないプロパティは、自身のプロパティと呼ばれます。obj には、自身のプロパティが 1 つあります (.objProp)。
一部の操作では、すべてのプロパティ (自身と継承されたプロパティ) を考慮します。たとえば、プロパティを取得する場合などです。
> const obj = { foo: 1 };
> typeof obj.foo // own
'number'
> typeof obj.toString // inherited
'function'その他の操作では、自身のプロパティのみを考慮します。たとえば、Object.keys() などです。
> Object.keys(obj)
[ 'foo' ]自身のプロパティのみを考慮する別の操作 (プロパティの設定) について、以下をお読みください。
プロトタイプチェーンの直感的ではない側面の 1 つは、オブジェクトを介して任意のプロパティ (継承されたプロパティでさえ) を設定すると、そのオブジェクト自体のみが変更され、プロトタイプのいずれかが変更されることはないということです。
次のオブジェクト obj を検討してください。
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};次のコードスニペットでは、継承されたプロパティ obj.protoProp (行 A) を設定します。これにより、自身のプロパティを作成することで "変更" されます。obj.protoProp を読み取ると、自身のプロパティが最初に検出され、その値が継承されたプロパティの値をオーバーライドします。
// In the beginning, obj has one own property
assert.deepEqual(Object.keys(obj), ['objProp']);
obj.protoProp = 'x'; // (A)
// We created a new own property:
assert.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
// The inherited property itself is unchanged:
assert.equal(proto.protoProp, 'a');
// The own property overrides the inherited property:
assert.equal(obj.protoProp, 'x');obj のプロトタイプチェーンは、図 11 に示されています。
__proto__ を避ける擬似プロパティ __proto__ を避けることをお勧めします。後で見るように、すべてのオブジェクトがそれを持っているわけではありません。
ただし、オブジェクトリテラル内の __proto__ は異なります。ここでは、組み込み機能であり、常に使用できます。
プロトタイプを取得および設定するための推奨される方法は次のとおりです。
プロトタイプを取得する最適な方法は、次のメソッドを使用することです。
Object.getPrototypeOf(obj: Object) : Objectプロトタイプを設定する最適な方法は、オブジェクトを作成するとき (オブジェクトリテラル内の __proto__ を使用するか、
Object.create(proto: Object) : Object必要に応じて、Object.setPrototypeOf() を使用して、既存のオブジェクトのプロトタイプを変更できます。ただし、パフォーマンスに悪影響を与える可能性があります。
これらの機能の使用方法を以下に示します。
const proto1 = {};
const proto2 = {};
const obj = Object.create(proto1);
assert.equal(Object.getPrototypeOf(obj), proto1);
Object.setPrototypeOf(obj, proto2);
assert.equal(Object.getPrototypeOf(obj), proto2);これまで、「p は o のプロトタイプである」という場合は、常に「p は o の直接のプロトタイプである」という意味でした。しかし、より緩く使用して、p が o のプロトタイプチェーン内にあるという意味で使用することもできます。より緩やかな関係は、次のようにして確認できます。
p.isPrototypeOf(o)たとえば
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);次のコードを検討してください。
const jane = {
name: 'Jane',
describe() {
return 'Person named '+this.name;
},
};
const tarzan = {
name: 'Tarzan',
describe() {
return 'Person named '+this.name;
},
};
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');非常によく似た 2 つのオブジェクトがあります。どちらにも .name と .describe という名前の 2 つのプロパティがあります。さらに、メソッド .describe() は同じです。このメソッドの複製を避けるにはどうすればよいでしょうか?
オブジェクト PersonProto に移動し、そのオブジェクトを jane と tarzan の両方のプロトタイプにすることができます。
const PersonProto = {
describe() {
return 'Person named ' + this.name;
},
};
const jane = {
__proto__: PersonProto,
name: 'Jane',
};
const tarzan = {
__proto__: PersonProto,
name: 'Tarzan',
};プロトタイプの名前は、jane と tarzan の両方が人物であることを反映しています。
図 12 は、3 つのオブジェクトがどのように接続されているかを示しています。下部のオブジェクトには、jane と tarzan に固有のプロパティが含まれるようになりました。上部のオブジェクトには、それらの間で共有されるプロパティが含まれています。
メソッド呼び出し jane.describe() を行うと、this はそのメソッド呼び出しのレシーバーである jane (図の左下隅) を指します。そのため、メソッドは引き続き機能します。tarzan.describe() も同様に機能します。
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');プロトタイプチェーンを設定するためのコンパクトな構文であるクラスに取り組む準備が整いました。内部的には、JavaScript のクラスは型破りです。しかし、それはクラスを扱うときにめったに見られないことです。他のオブジェクト指向プログラミング言語を使用したことがある人にとっては、通常は使い慣れているはずです。
以前は、人物を表す単一のオブジェクトである jane と tarzan を使用していました。クラス宣言を使用して、人物オブジェクトのファクトリーを実装しましょう。
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}jane と tarzan は、new Person() を使用して作成できるようになりました。
const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');
assert.equal(jane.describe(), 'Person named Jane');
const tarzan = new Person('Tarzan');
assert.equal(tarzan.name, 'Tarzan');
assert.equal(tarzan.describe(), 'Person named Tarzan');クラス Person には、2 つのメソッドがあります。
.describe().constructor()。クラス名の後に new 演算子に渡される引数を受け取ります。新しいインスタンスを設定するために引数が不要な場合は、コンストラクターを省略できます。クラス定義 (クラスを定義する方法) には、2 つの種類があります。
クラス式には、匿名と名前付きの 2 つがあります。
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };名前付きクラス式の名前は、名前付き関数式の名前と同様に機能します。
これはクラスの最初の一瞥でした。すぐにさらに多くの機能を調べますが、最初にクラスの内部構造を学習する必要があります。
クラスの内部では多くのことが起こっています。jane の図を見てみましょう (図 13)。
クラス Person の主な目的は、右側のプロトタイプチェーン (jane の後に Person.prototype が続く) を設定することです。クラス Person 内の両方の構成要素 (.constructor と .describe()) が、Person ではなく、Person.prototype のプロパティを作成したことに注目するのは興味深いことです。
この少し奇妙なアプローチの理由は、下位互換性です。クラスの前は、コンストラクター関数 (通常の関数。new 演算子を介して呼び出される) がオブジェクトのファクトリーとしてよく使用されていました。クラスは、コンストラクター関数に対するほとんどが優れた構文であるため、古いコードとの互換性を維持しています。それがクラスが関数である理由です。
> typeof Person
'function'本書では、コンストラクター (関数) とクラスという用語を同義で使用します。
.__proto__ と .prototype を混同しやすいです。図 13 で、それらの違いが明確になることを願っています。
.__proto__ は、オブジェクトのプロトタイプにアクセスするための擬似プロパティです。.prototype は、new 演算子の使用方法によってのみ特殊となる通常のプロパティです。名前は理想的ではありません。Person.prototype は Person のプロトタイプを指すのではなく、Person のすべてのインスタンスのプロトタイプを指します。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
// (the instanceof operator is explained later)
assert.equal(cheeta instanceof Person, true);次に、与えられたインスタンスを作成したクラスの名前を取得できます。
const tarzan = new Person('Tarzan');
assert.equal(tarzan.constructor.name, 'Person');次のクラス宣言の本体にあるすべての構造は、Foo.prototypeのプロパティを作成します。
class Foo {
constructor(prop) {
this.prop = prop;
}
protoMethod() {
return 'protoMethod';
}
get protoGetter() {
return 'protoGetter';
}
}順番に見ていきましょう。
.constructor()は、Fooの新しいインスタンスを作成した後、そのインスタンスを設定するために呼び出されます。.protoMethod()は通常のメソッドです。これはFoo.prototypeに保存されます。.protoGetterはFoo.prototypeに保存されるgetterです。次のインタラクションでは、クラスFooを使用します。
> const foo = new Foo(123);
> foo.prop
123
> foo.protoMethod()
'protoMethod'
> foo.protoGetter
'protoGetter'次のクラス宣言の本体にあるすべての構造は、いわゆる静的プロパティ、つまりBar自体のプロパティを作成します。
class Bar {
static staticMethod() {
return 'staticMethod';
}
static get staticGetter() {
return 'staticGetter';
}
}静的メソッドと静的ゲッターは次のように使用されます。
> Bar.staticMethod()
'staticMethod'
> Bar.staticGetter
'staticGetter'instanceof演算子instanceof演算子は、値が与えられたクラスのインスタンスであるかどうかを判断します。
> new Person('Jane') instanceof Person
true
> ({}) instanceof Person
false
> ({}) instanceof Object
true
> [] instanceof Array
trueサブクラス化を見た後で、instanceof演算子について後ほど詳しく見ていきます。
次の理由から、クラスの使用をお勧めします。
クラスは、オブジェクトの作成と継承のための共通の標準であり、現在ではフレームワーク(React、Angular、Emberなど)全体で広くサポートされています。これは、ほぼすべてのフレームワークが独自の継承ライブラリを持っていた以前の状態からの改善です。
IDEや型チェッカーなどのツールが作業を行うのを助け、そこで新しい機能を利用できるようにします。
別の言語からJavaScriptに来てクラスに慣れている場合は、より早く始めることができます。
JavaScriptエンジンはそれらを最適化します。つまり、クラスを使用するコードは、カスタムの継承ライブラリを使用するコードよりもほぼ常に高速です。
Errorなどの組み込みコンストラクタ関数をサブクラス化できます。
それは、クラスが完璧であることを意味するものではありません。
過剰な継承のリスクがあります。
クラスに多くの機能を入れすぎるリスクがあります(その一部は多くの場合、関数に入れる方が優れています)。
表面的および内部での動作はかなり異なります。言い換えれば、構文とセマンティクスには乖離があります。2つの例は次のとおりです。
C内のメソッド定義は、オブジェクトC.prototypeにメソッドを作成します。この乖離の動機は、後方互換性です。ありがたいことに、この乖離は実際にはほとんど問題を引き起こしません。クラスが意図しているように進めれば、通常は問題ありません。
演習: クラスの作成
exercises/proto-chains-classes/point_class_test.mjs
このセクションでは、オブジェクトの一部のデータを外部から隠すためのテクニックについて説明します。クラスのコンテキストでそれらについて説明しますが、オブジェクトリテラルなどを介して直接作成されたオブジェクトにも有効です。
最初のテクニックは、名前の前にアンダースコアを付けることでプロパティをプライベートにします。これはプロパティを保護するものではなく、単に外部に「このプロパティについて知る必要はない」というシグナルを送るだけです。
次のコードでは、プロパティ._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']);このテクニックでは、保護はまったく得られず、プライベートな名前が衝突する可能性があります。利点としては、使いやすいことが挙げられます。
もう1つのテクニックはWeakMapsを使用することです。それがどのように正確に機能するかについては、WeakMapsに関する章で説明します。これはプレビューです。
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
// The two pseudo-properties are truly private:
assert.deepEqual(
Object.keys(new Countdown()),
[]);このテクニックは、外部からのアクセスに対してかなりの保護を提供し、名前の衝突も発生しません。しかし、使用するのはより複雑です。
この本では、クラスのプライベートデータに関する最も重要なテクニックを説明します。また、おそらくすぐに組み込みサポートも提供されるでしょう。詳細については、ECMAScriptの提案“クラスの公開インスタンスフィールドとプライベートインスタンスフィールド”を参照してください。
いくつかの追加のテクニックは、Exploring ES6で説明されています。
クラスは、既存のクラスをサブクラス化(「拡張」)することもできます。例として、次のクラスEmployeeはPersonをサブクラス化します。
class Person {
constructor(name) {
this.name = name;
}
describe() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.describe(),
'Person named Jane (CTO)');2つのコメント
.constructor()メソッド内では、thisにアクセスする前に、super()を介してスーパークラスのコンストラクタを呼び出す必要があります。これは、スーパークラスのコンストラクタが呼び出されるまでthisが存在しないためです(この現象はクラスに固有のものです)。
静的メソッドも継承されます。たとえば、Employeeは静的メソッド.logNames()を継承します。
> 'logNames' in Employee
true 演習: サブクラス化
exercises/proto-chains-classes/color_point_class_test.mjs
前のセクションのクラスPersonとEmployeeは、いくつかのオブジェクトで構成されています(図 14)。これらのオブジェクトがどのように関連しているかを理解するための重要な洞察の1つは、2つのプロトタイプチェーンがあることです。
インスタンスプロトタイプチェーンはjaneで始まり、Employee.prototype、Person.prototypeと続きます。原則として、プロトタイプチェーンはここで終わりますが、もう1つのオブジェクト、Object.prototypeが得られます。このプロトタイプは、ほぼすべてのオブジェクトにサービスを提供するため、ここにも含まれています。
> Object.getPrototypeOf(Person.prototype) === Object.prototype
trueクラスプロトタイプチェーンでは、最初にEmployee、次にPersonが来ます。その後、チェーンはFunction.prototypeへと続きます。これは、Personが関数であり、関数にはFunction.prototypeのサービスが必要なためだけに存在します。
> Object.getPrototypeOf(Person) === Function.prototype
trueinstanceofの詳細(高度な内容)instanceofが実際にどのように機能するかはまだ見ていません。式が与えられた場合
x instanceof Cinstanceofは、xがCのインスタンス(またはCのサブクラス)であるかどうかをどのように判断しますか?これは、C.prototypeがxのプロトタイプチェーン内にあるかどうかを確認することによって行います。つまり、次の式は同等です。
C.prototype.isPrototypeOf(x)図 14に戻ると、プロトタイプチェーンが次の正しい答えに導いてくれることを確認できます。
> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
true次に、サブクラス化の知識を使用して、いくつかの組み込みオブジェクトのプロトタイプチェーンを理解します。次のツール関数p()が調査に役立ちます。
const p = Object.getPrototypeOf.bind(Object);Objectのメソッド.getPrototypeOf()を抽出し、pに割り当てました。
{}のプロトタイプチェーンまず、プレーンオブジェクトを調べてみましょう。
> p({}) === Object.prototype
true
> p(p({})) === null
true図 15は、このプロトタイプチェーンの図を示しています。{}が実際にObjectのインスタンスであること、つまりObject.prototypeがそのプロトタイプチェーン内にあることがわかります。
[]のプロトタイプチェーンArrayのプロトタイプチェーンはどのようになっているでしょうか。
> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
trueこのプロトタイプチェーン(図 16で視覚化)は、ArrayオブジェクトがArrayのインスタンスであり、Objectのサブクラスであることを示しています。
function () {}のプロトタイプチェーン最後に、通常の関数のプロトタイプチェーンは、すべての関数がオブジェクトであることを示しています。
> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
trueObjectのインスタンスではないオブジェクトObject.prototypeがそのプロトタイプチェーンにある場合のみ、オブジェクトはObjectのインスタンスです。さまざまなリテラルを介して作成されたほとんどのオブジェクトは、Objectのインスタンスです。
> ({}) instanceof Object
true
> (() => {}) instanceof Object
true
> /abc/ug instanceof Object
trueプロトタイプを持たないオブジェクトは、Objectのインスタンスではありません。
> ({ __proto__: null }) instanceof Object
falseObject.prototypeはほとんどのプロトタイプチェーンを終わらせます。そのプロトタイプはnullであり、これはObjectのインスタンスでもないことを意味します。
> Object.prototype instanceof Object
false.__proto__は正確にはどのように機能しますか?疑似プロパティ.__proto__は、getterとsetterを介してクラスObjectによって実装されます。これは次のように実装できます。
class Object {
get __proto__() {
return Object.getPrototypeOf(this);
}
set __proto__(other) {
Object.setPrototypeOf(this, other);
}
// ···
}つまり、プロトタイプチェーンにObject.prototypeがないオブジェクトを作成することで、.__proto__をオフにすることができます(前のセクションを参照)。
> '__proto__' in {}
true
> '__proto__' in { __proto__: null }
falseクラスでのメソッド呼び出しのしくみを調べてみましょう。以前のjaneを再訪します。
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}
const jane = new Person('Jane');図 17は、janeのプロトタイプチェーンの図を示しています。
通常のメソッド呼び出しはディスパッチされます。つまり、メソッド呼び出しjane.describe()は2つのステップで発生します。
ディスパッチ: janeのプロトタイプチェーンで、キーが'describe'である最初のプロパティを見つけて、その値を取得します。
const func = jane.describe;呼び出し: 値を呼び出し、同時にthisをjaneに設定します。
func.call(jane);このメソッドを動的に探し出して呼び出す方法は、動的ディスパッチと呼ばれます。
ディスパッチせずに、直接同じメソッド呼び出しを行うことができます。
Person.prototype.describe.call(jane)今回は、Person.prototype.describeを介してメソッドを直接指定し、プロトタイプチェーンで検索しません。また、.call()を介してthisを異なる方法で指定します。
this は常にプロトタイプチェーンの先頭を指すことに注意してください。これにより、.describe() は .name にアクセスできます。
Object.prototype のメソッドを扱う場合、直接メソッド呼び出しが役立ちます。たとえば、Object.prototype.hasOwnProperty(k) は、this がキーが k である非継承プロパティを持っているかどうかを確認します。
> const obj = { foo: 123 };
> obj.hasOwnProperty('foo')
true
> obj.hasOwnProperty('bar')
falseただし、オブジェクトのプロトタイプチェーンには、Object.prototype のメソッドをオーバーライドするキー 'hasOwnProperty' を持つ別のプロパティが存在する可能性があります。その場合、ディスパッチされたメソッド呼び出しは機能しません。
> const obj = { hasOwnProperty: true };
> obj.hasOwnProperty('bar')
TypeError: obj.hasOwnProperty is not a function回避策は、直接メソッド呼び出しを使用することです。
> Object.prototype.hasOwnProperty.call(obj, 'bar')
false
> Object.prototype.hasOwnProperty.call(obj, 'hasOwnProperty')
trueこの種の直接メソッド呼び出しは、次のように省略されることがよくあります。
> ({}).hasOwnProperty.call(obj, 'bar')
false
> ({}).hasOwnProperty.call(obj, 'hasOwnProperty')
trueこのパターンは非効率的に見えるかもしれませんが、ほとんどのエンジンはこのパターンを最適化するため、パフォーマンスの問題にはならないはずです。
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)) {
/*···*/
}これで、Object(ほとんどのクラスが暗黙的に行う)を拡張するクラス S1 を拡張するクラス S2 を拡張するクラス C ができました。
オブジェクトのブランドを設定および取得するためのヘルパーメソッドを持つミックスイン Branded を実装します。
const Branded = (Sup) => class extends Sup {
setBrand(brand) {
this._brand = brand;
return this;
}
getBrand() {
return this._brand;
}
};このミックスインを使用して、クラス Car のブランド管理を実装します。
class Car extends Branded(Object) {
constructor(model) {
super();
this._model = model;
}
toString() {
return `${this.getBrand()} ${this._model}`;
}
}次のコードは、ミックスインが機能したことを確認します。Car には Branded のメソッド .setBrand() があります。
const modelT = new Car('Model T').setBrand('Ford');
assert.equal(modelT.toString(), 'Ford Model T');ミックスインは、単一継承の制約から私たちを解放します。
原則として、オブジェクトは順序付けられていません。プロパティを順序付けする主な理由は、エントリ、キー、または値をリストする操作が確定的になるようにするためです。これは、たとえばテストに役立ちます。
クイズ
クイズアプリ を参照してください。