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:
.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true); assert
プロトタイプオブジェクト自体がプロトタイプを持つことができるため、オブジェクトのチェーン (いわゆるプロトタイプチェーン) が作成されます。つまり、継承によって単一のオブジェクトを扱っているように見えますが、実際にはオブジェクトのチェーンを扱っています。
図 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
.deepEqual(Object.keys(obj), ['objProp']);
assert
.protoProp = 'x'; // (A)
obj
// We created a new own property:
.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
assert
// The inherited property itself is unchanged:
.equal(proto.protoProp, 'a');
assert
// The own property overrides the inherited property:
.equal(obj.protoProp, 'x'); assert
obj
のプロトタイプチェーンは、図 11 に示されています。
__proto__
を避ける擬似プロパティ __proto__
を避けることをお勧めします。後で見るように、すべてのオブジェクトがそれを持っているわけではありません。
ただし、オブジェクトリテラル内の __proto__
は異なります。ここでは、組み込み機能であり、常に使用できます。
プロトタイプを取得および設定するための推奨される方法は次のとおりです。
プロトタイプを取得する最適な方法は、次のメソッドを使用することです。
.getPrototypeOf(obj: Object) : Object Object
プロトタイプを設定する最適な方法は、オブジェクトを作成するとき (オブジェクトリテラル内の __proto__
を使用するか、
.create(proto: Object) : Object Object
必要に応じて、Object.setPrototypeOf()
を使用して、既存のオブジェクトのプロトタイプを変更できます。ただし、パフォーマンスに悪影響を与える可能性があります。
これらの機能の使用方法を以下に示します。
const proto1 = {};
const proto2 = {};
const obj = Object.create(proto1);
.equal(Object.getPrototypeOf(obj), proto1);
assert
Object.setPrototypeOf(obj, proto2);
.equal(Object.getPrototypeOf(obj), proto2); assert
これまで、「p
は o
のプロトタイプである」という場合は、常に「p
は o
の直接のプロトタイプである」という意味でした。しかし、より緩く使用して、p
が o
のプロトタイプチェーン内にあるという意味で使用することもできます。より緩やかな関係は、次のようにして確認できます。
.isPrototypeOf(o) p
たとえば
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
次のコードを検討してください。
const jane = {
name: 'Jane',
describe() {
return 'Person named '+this.name;
,
};
}const tarzan = {
name: 'Tarzan',
describe() {
return 'Person named '+this.name;
,
};
}
.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
非常によく似た 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()
も同様に機能します。
.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
プロトタイプチェーンを設定するためのコンパクトな構文であるクラスに取り組む準備が整いました。内部的には、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');
.equal(jane.name, 'Jane');
assert.equal(jane.describe(), 'Person named Jane');
assert
const tarzan = new Person('Tarzan');
.equal(tarzan.name, 'Tarzan');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
クラス 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 === Persontrue
この設定も下位互換性のために存在します。ただし、追加のメリットが 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)
.equal(cheeta instanceof Person, true); assert
次に、与えられたインスタンスを作成したクラスの名前を取得できます。
const tarzan = new Person('Tarzan');
.equal(tarzan.constructor.name, 'Person'); assert
次のクラス宣言の本体にあるすべての構造は、Foo.prototype
のプロパティを作成します。
class Foo {
constructor(prop) {
this.prop = prop;
}protoMethod() {
return 'protoMethod';
}protoGetter() {
get return 'protoGetter';
} }
順番に見ていきましょう。
.constructor()
は、Foo
の新しいインスタンスを作成した後、そのインスタンスを設定するために呼び出されます。.protoMethod()
は通常のメソッドです。これはFoo.prototype
に保存されます。.protoGetter
はFoo.prototype
に保存されるgetterです。次のインタラクションでは、クラスFoo
を使用します。
> const foo = new Foo(123);
> foo.prop123
> 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 Persontrue
> ({}) instanceof Personfalse
> ({}) instanceof Objecttrue
> [] instanceof Arraytrue
サブクラス化を見た後で、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:
.deepEqual(
assertObject.keys(new Countdown()),
'_counter', '_action']); [
このテクニックでは、保護はまったく得られず、プライベートな名前が衝突する可能性があります。利点としては、使いやすいことが挙げられます。
もう1つのテクニックはWeakMapsを使用することです。それがどのように正確に機能するかについては、WeakMapsに関する章で説明します。これはプレビューです。
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()),
; [])
このテクニックは、外部からのアクセスに対してかなりの保護を提供し、名前の衝突も発生しません。しかし、使用するのはより複雑です。
この本では、クラスのプライベートデータに関する最も重要なテクニックを説明します。また、おそらくすぐに組み込みサポートも提供されるでしょう。詳細については、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');
.equal(
assert.describe(),
jane'Person named Jane (CTO)');
2つのコメント
.constructor()
メソッド内では、this
にアクセスする前に、super()
を介してスーパークラスのコンストラクタを呼び出す必要があります。これは、スーパークラスのコンストラクタが呼び出されるまでthis
が存在しないためです(この現象はクラスに固有のものです)。
静的メソッドも継承されます。たとえば、Employee
は静的メソッド.logNames()
を継承します。
> 'logNames' in Employeetrue
演習: サブクラス化
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.prototypetrue
クラスプロトタイプチェーンでは、最初にEmployee
、次にPerson
が来ます。その後、チェーンはFunction.prototype
へと続きます。これは、Person
が関数であり、関数にはFunction.prototype
のサービスが必要なためだけに存在します。
> Object.getPrototypeOf(Person) === Function.prototypetrue
instanceof
の詳細(高度な内容)instanceof
が実際にどのように機能するかはまだ見ていません。式が与えられた場合
instanceof C x
instanceof
は、x
がC
のインスタンス(またはC
のサブクラス)であるかどうかをどのように判断しますか?これは、C.prototype
がx
のプロトタイプチェーン内にあるかどうかを確認することによって行います。つまり、次の式は同等です。
.prototype.isPrototypeOf(x) C
図 14に戻ると、プロトタイプチェーンが次の正しい答えに導いてくれることを確認できます。
> jane instanceof Employeetrue
> jane instanceof Persontrue
> jane instanceof Objecttrue
次に、サブクラス化の知識を使用して、いくつかの組み込みオブジェクトのプロトタイプチェーンを理解します。次のツール関数p()
が調査に役立ちます。
const p = Object.getPrototypeOf.bind(Object);
Object
のメソッド.getPrototypeOf()
を抽出し、p
に割り当てました。
{}
のプロトタイプチェーンまず、プレーンオブジェクトを調べてみましょう。
> p({}) === Object.prototypetrue
> p(p({})) === nulltrue
図 15は、このプロトタイプチェーンの図を示しています。{}
が実際にObject
のインスタンスであること、つまりObject.prototype
がそのプロトタイプチェーン内にあることがわかります。
[]
のプロトタイプチェーンArrayのプロトタイプチェーンはどのようになっているでしょうか。
> p([]) === Array.prototypetrue
> p(p([])) === Object.prototypetrue
> p(p(p([]))) === nulltrue
このプロトタイプチェーン(図 16で視覚化)は、ArrayオブジェクトがArray
のインスタンスであり、Object
のサブクラスであることを示しています。
function () {}
のプロトタイプチェーン最後に、通常の関数のプロトタイプチェーンは、すべての関数がオブジェクトであることを示しています。
> p(function () {}) === Function.prototypetrue
> p(p(function () {})) === Object.prototypetrue
Object
のインスタンスではないオブジェクトObject.prototype
がそのプロトタイプチェーンにある場合のみ、オブジェクトはObject
のインスタンスです。さまざまなリテラルを介して作成されたほとんどのオブジェクトは、Object
のインスタンスです。
> ({}) instanceof Objecttrue
> (() => {}) instanceof Objecttrue
> /abc/ug instanceof Objecttrue
プロトタイプを持たないオブジェクトは、Object
のインスタンスではありません。
> ({ __proto__: null }) instanceof Objectfalse
Object.prototype
はほとんどのプロトタイプチェーンを終わらせます。そのプロトタイプはnull
であり、これはObject
のインスタンスでもないことを意味します。
> Object.prototype instanceof Objectfalse
.__proto__
は正確にはどのように機能しますか?疑似プロパティ.__proto__
は、getterとsetterを介してクラスObject
によって実装されます。これは次のように実装できます。
class Object {
__proto__() {
get return Object.getPrototypeOf(this);
}__proto__(other) {
set 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
に設定します。
.call(jane); func
このメソッドを動的に探し出して呼び出す方法は、動的ディスパッチと呼ばれます。
ディスパッチせずに、直接同じメソッド呼び出しを行うことができます。
.prototype.describe.call(jane) Person
今回は、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');
.equal(modelT.toString(), 'Ford Model T'); assert
ミックスインは、単一継承の制約から私たちを解放します。
原則として、オブジェクトは順序付けられていません。プロパティを順序付けする主な理由は、エントリ、キー、または値をリストする操作が確定的になるようにするためです。これは、たとえばテストに役立ちます。
クイズ
クイズアプリ を参照してください。