JavaScriptのプロトタイプと継承を理解する
序章
JavaScriptはプロトタイプベースの言語です。つまり、オブジェクトのプロパティとメソッドは、複製および拡張できる一般化されたオブジェクトを介して共有できます。 これはプロトタイプ継承と呼ばれ、クラス継承とは異なります。 PHP、Python、Javaなどの他の著名な言語はクラスベースの言語であり、代わりにクラスをオブジェクトの青写真として定義するため、人気のあるオブジェクト指向プログラミング言語の中で、JavaScriptは比較的ユニークです。
このチュートリアルでは、オブジェクトプロトタイプとは何か、およびコンストラクター関数を使用してプロトタイプを新しいオブジェクトに拡張する方法を学習します。 また、継承とプロトタイプチェーンについても学びます。
JavaScriptプロトタイプ
JavaScriptでのオブジェクトの理解では、オブジェクトのデータ型、オブジェクトの作成方法、およびオブジェクトのプロパティへのアクセスと変更の方法について説明しました。 次に、プロトタイプを使用してオブジェクトを拡張する方法を学習します。
JavaScriptのすべてのオブジェクトには、Prototypeという内部プロパティがあります。 これは、新しい空のオブジェクトを作成することで実証できます。
let x = {};
これは通常オブジェクトを作成する方法ですが、これを実現する別の方法はオブジェクトコンストラクターlet x = new Object()を使用することです。
Prototypeを囲む二重角かっこは、それが内部プロパティであり、コードで直接アクセスできないことを示します。
この新しく作成されたオブジェクトのPrototypeを見つけるには、getPrototypeOf()メソッドを使用します。
Object.getPrototypeOf(x);
出力は、いくつかの組み込みプロパティとメソッドで構成されます。
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
Prototypeを見つける別の方法は、__proto__プロパティを使用することです。 __ proto __ は、オブジェクトの内部Prototypeを公開するプロパティです。
.__proto__はレガシー機能であり、本番コードでは使用しないでください。また、最新のすべてのブラウザーに存在するわけではないことに注意してください。 ただし、この記事全体で説明目的で使用できます。
x.__proto__;
出力は、getPrototypeOf()を使用した場合と同じになります。
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
JavaScriptのすべてのオブジェクトにPrototypeがあることが重要です。これは、2つ以上のオブジェクトをリンクする方法を作成するためです。
作成するオブジェクトには、DateやArrayなどの組み込みオブジェクトと同様に、Prototypeがあります。 このチュートリアルの後半で説明するように、prototypeプロパティを介して、あるオブジェクトから別のオブジェクトへのこの内部プロパティへの参照を行うことができます。
プロトタイプの継承
オブジェクトのプロパティまたはメソッドにアクセスしようとすると、JavaScriptは最初にオブジェクト自体を検索し、見つからない場合はオブジェクトのPrototypeを検索します。 オブジェクトとそのPrototypeの両方を調べても一致するものが見つからない場合、JavaScriptはリンクされたオブジェクトのプロトタイプをチェックし、プロトタイプチェーンの最後に到達するまで検索を続けます。
プロトタイプチェーンの最後には、Object.prototypeがあります。 すべてのオブジェクトは、Objectのプロパティとメソッドを継承します。 チェーンの終わりを超えて検索しようとすると、nullになります。
この例では、xはObjectから継承する空のオブジェクトです。 xは、toString()など、Objectが持つ任意のプロパティまたはメソッドを使用できます。
x.toString();
Output[object Object]
このプロトタイプチェーンの長さは1リンクだけです。 x->Object。 これは、2つのPrototypeプロパティをチェーン化しようとすると、nullになるためです。
x.__proto__.__proto__;
Outputnull
別のタイプのオブジェクトを見てみましょう。 JavaScript での配列の操作の経験がある場合は、pop()やpush()などの多くの組み込みメソッドがあることをご存知でしょう。 新しい配列を作成するときにこれらのメソッドにアクセスできる理由は、作成するすべての配列がArray.prototypeのプロパティとメソッドにアクセスできるためです。
新しい配列を作成することでこれをテストできます。
let y = [];
配列コンストラクターlet y = new Array()としても記述できることに注意してください。
新しいy配列のPrototypeを見ると、xオブジェクトよりも多くのプロパティとメソッドがあることがわかります。 Array.prototypeからすべてを継承しています。
y.__proto__;
[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]
Array()に設定されているプロトタイプのconstructorプロパティに気付くでしょう。 constructorプロパティは、オブジェクトのコンストラクター関数を返します。これは、関数からオブジェクトを構築するために使用されるメカニズムです。
この場合、プロトタイプチェーンが長くなるため、2つのプロトタイプをチェーンできます。 y-> Array->Objectのように見えます。
y.__proto__.__proto__;
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
このチェーンは現在Object.prototypeを参照しています。 内部のPrototypeをコンストラクター関数のprototypeプロパティに対してテストして、同じものを参照していることを確認できます。
y.__proto__ === Array.prototype; // true y.__proto__.__proto__ === Object.prototype; // true
isPrototypeOf()メソッドを使用してこれを実現することもできます。
Array.prototype.isPrototypeOf(y); // true Object.prototype.isPrototypeOf(Array); // true
instanceof演算子を使用して、コンストラクターのprototypeプロパティがオブジェクトのプロトタイプチェーン内のどこかに表示されるかどうかをテストできます。
y instanceof Array; // true
要約すると、すべてのJavaScriptオブジェクトには、非表示の内部Prototypeプロパティがあります(一部のブラウザーでは、__proto__を介して公開される場合があります)。 オブジェクトは拡張可能であり、コンストラクターのPrototypeのプロパティとメソッドを継承します。
これらのプロトタイプはチェーン化することができ、追加の各オブジェクトはチェーン全体ですべてを継承します。 チェーンはObject.prototypeで終わります。
コンストラクター関数
コンストラクター関数は、新しいオブジェクトを作成するために使用される関数です。 new演算子は、コンストラクター関数に基づいて新しいインスタンスを作成するために使用されます。 new Array()やnew Date()などの組み込みのJavaScriptコンストラクターを見てきましたが、新しいオブジェクトを作成するための独自のカスタムテンプレートを作成することもできます。
例として、非常にシンプルなテキストベースのロールプレイングゲームを作成しているとしましょう。 ユーザーはキャラクターを選択してから、戦士、ヒーラー、泥棒など、どのキャラクタークラスを使用するかを選択できます。
各キャラクターは、名前、レベル、ヒットポイントなど、多くの特性を共有するため、コンストラクターをテンプレートとして作成することは理にかなっています。 ただし、各キャラクタークラスの能力は大きく異なる可能性があるため、各キャラクターが自分の能力にのみアクセスできるようにする必要があります。 プロトタイプの継承とコンストラクターを使用してこれを実現する方法を見てみましょう。
まず、コンストラクター関数は単なる通常の関数です。 newキーワードを使用してインスタンスから呼び出されると、コンストラクターになります。 JavaScriptでは、慣例によりコンストラクター関数の最初の文字を大文字にします。
characterSelect.js
// Initialize a constructor function for a new Hero
function Hero(name, level) {
this.name = name;
this.level = level;
}
nameとlevelの2つのパラメーターを使用して、Heroというコンストラクター関数を作成しました。 すべてのキャラクターには名前とレベルがあるので、新しいキャラクターごとにこれらのプロパティを持つことは理にかなっています。 thisキーワードは作成された新しいインスタンスを参照するため、this.nameをnameパラメーターに設定すると、新しいオブジェクトにnameプロパティが設定されます。 。
これで、newを使用して新しいインスタンスを作成できます。
let hero1 = new Hero('Bjorn', 1);
hero1をコンソールアウトすると、新しいプロパティが期待どおりに設定された新しいオブジェクトが作成されたことがわかります。
OutputHero {name: "Bjorn", level: 1}
ここで、hero1のPrototypeを取得すると、constructorはHero()として表示されます。 (これはhero1.__proto__と同じ入力ですが、使用するのに適切な方法であることを忘れないでください。)
Object.getPrototypeOf(hero1);
Outputconstructor: ƒ Hero(name, level)
コンストラクターでメソッドを定義せず、プロパティのみを定義していることに気付くかもしれません。 効率とコードの可読性を高めるために、プロトタイプでメソッドを定義することはJavaScriptの一般的な方法です。
prototypeを使用して、Heroにメソッドを追加できます。 greet()メソッドを作成します。
characterSelect.js
...
// Add greet method to the Hero prototype
Hero.prototype.greet = function () {
return `${this.name} says hello.`;
}
greet()はHeroのprototypeにあり、hero1はHeroのインスタンスであるため、このメソッドはhero1。
hero1.greet();
Output"Bjorn says hello."
ヒーローのPrototypeを調べると、greet()が利用可能なオプションとして表示されます。
これは良いことですが、ヒーローが使用するキャラクタークラスを作成したいと思います。 クラスが異なれば能力も異なるため、すべてのクラスのすべての能力をHeroコンストラクターに入れるのは意味がありません。 新しいコンストラクター関数を作成したいが、それらを元のHeroに接続したい。
call()メソッドを使用して、あるコンストラクターから別のコンストラクターにプロパティをコピーできます。 WarriorとHealerのコンストラクターを作成しましょう。
characterSelect.js
...
// Initialize Warrior constructor
function Warrior(name, level, weapon) {
// Chain constructor with call
Hero.call(this, name, level);
// Add a new property
this.weapon = weapon;
}
// Initialize Healer constructor
function Healer(name, level, spell) {
Hero.call(this, name, level);
this.spell = spell;
}
新しいコンストラクターは両方とも、Heroのプロパティといくつかのunqiueプロパティを持っています。 attack()メソッドをWarriorに追加し、heal()メソッドをHealerに追加します。
characterSelect.js
...
Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`;
}
Healer.prototype.heal = function () {
return `${this.name} casts ${this.spell}.`;
}
この時点で、利用可能な2つの新しいキャラクタークラスを使用してキャラクターを作成します。
characterSelect.js
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');
hero1は、新しいプロパティを持つWarriorとして認識されるようになりました。
OutputWarrior {name: "Bjorn", level: 1, weapon: "axe"}
Warriorプロトタイプに設定した新しいメソッドを使用できます。
hero1.attack();
Console"Bjorn attacks with the axe."
しかし、プロトタイプチェーンのさらに下流でメソッドを使用しようとするとどうなりますか?
hero1.greet();
OutputUncaught TypeError: hero1.greet is not a function
call()を使用してコンストラクターをチェーンする場合、プロトタイプのプロパティとメソッドは自動的にリンクされません。 Object.setPropertyOf()を使用して、HeroコンストラクターのプロパティをWarriorおよびHealerコンストラクターにリンクし、追加のメソッドの前に配置するようにします。
characterSelect.js
... Object.setPrototypeOf(Warrior.prototype, Hero.prototype); Object.setPrototypeOf(Healer.prototype, Hero.prototype); // All other prototype methods added below ...
これで、WarriorまたはHealerのインスタンスでHeroのプロトタイプメソッドを正常に使用できるようになりました。
hero1.greet();
Output"Bjorn says hello."
これがキャラクター作成ページの完全なコードです。
characterSelect.js
// Initialize constructor functions
function Hero(name, level) {
this.name = name;
this.level = level;
}
function Warrior(name, level, weapon) {
Hero.call(this, name, level);
this.weapon = weapon;
}
function Healer(name, level, spell) {
Hero.call(this, name, level);
this.spell = spell;
}
// Link prototypes and add prototype methods
Object.setPrototypeOf(Warrior.prototype, Hero.prototype);
Object.setPrototypeOf(Healer.prototype, Hero.prototype);
Hero.prototype.greet = function () {
return `${this.name} says hello.`;
}
Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`;
}
Healer.prototype.heal = function () {
return `${this.name} casts ${this.spell}.`;
}
// Initialize individual character instances
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');
このコードを使用して、基本プロパティを使用してHeroコンストラクターを作成し、元のコンストラクターからWarriorおよびHealerという2つの文字コンストラクターを作成し、プロトタイプにメソッドを追加して作成しました。個々の文字インスタンス。
結論
JavaScriptはプロトタイプベースの言語であり、他の多くのオブジェクト指向言語が使用する従来のクラスベースのパラダイムとは機能が異なります。
このチュートリアルでは、JavaScriptでプロトタイプがどのように機能するか、およびすべてのオブジェクトが共有する非表示のPrototypeプロパティを介してオブジェクトのプロパティとメソッドをリンクする方法を学習しました。 また、カスタムコンストラクター関数を作成する方法と、プロトタイプの継承がプロパティとメソッドの値を渡すためにどのように機能するかについても学びました。