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
プロパティを介してオブジェクトのプロパティとメソッドをリンクする方法を学習しました。 また、カスタムコンストラクター関数を作成する方法と、プロトタイプの継承がプロパティとメソッドの値を渡すためにどのように機能するかについても学びました。