LoginSignup
3
3

More than 3 years have passed since last update.

この記事では、JavaScriptのオブジェクトのプロトタイプ、その継承とチェーンについて説明するとともに、新しいプロトタイプを使ってオブジェクトを拡張するためにコンストラクタをどのように使うかを学びます。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

JavaScriptでのプロトタイプ

JSにおけるオブジェクトのデータ型、オブジェクトの作成、オブジェクトのプロパティの変更方法などについては、すでにご存知の方が多いと思います。プロトタイプを使用すると、各JavaScriptオブジェクトが内部プロパティとして[[プロトタイプ]]を含むという事実に基づいてオブジェクトを拡張することができます。これを実際に見るために、次のような空のプロジェクトを作成してみましょう。

let x = {};

これはオブジェクトを作成するための実用的なメソッドですが、このようにオブジェクトのコンストラクタを使うアプローチもあります。

let x = new Object().

プロパティ[[Prototype]]には二重の角括弧が付いていますが、これはその内部性を示すもので、つまりコード内では直接アクセスできません。したがって、オブジェクトの[[プロトタイプ]]を取得するにはgetPrototypeOf()メソッドを使用する必要があります。

Object.getPrototypeOf(x);

このメソッドは以下の組み込みプロパティとメソッドを生成します。

Output
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

protoプロパティを使って[[プロトタイプ]]を調べるのももっともです。protoメソッドはまた、オブジェクトの内部プロパティを生成します。

protoは、すべての利用可能なブラウザと普遍的に互換性があるわけではないレガシーな機能なので、プロダクションコードでは使用すべきではないことに注意してください。それにもかかわらず、このチュートリアルの目的のために、デモのために使用することはできますが、前記の感情を念頭に置いてください。

x.__proto__;

PrototypeOf()を使用した場合と同じ出力が得られます。

Output
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

理想的には、JavaScriptのすべてのオブジェクトは、2つ以上のオブジェクトをリンクできるように[[プロトタイプ]]を持つべきです。

作成されたすべてのJavaScriptオブジェクトや組み込みオブジェクトは、[[プロトタイプ]]プロパティで構成されています。この内部プロパティを使って、さらに下の方で探ってきたように、プロトタイププロパティを使って、お互いの間のオブジェクトを参照することができます。

継承

JavaScriptは、あなたがそのプロパティやメソッドにアクセスしようとするたびに、最初にオブジェクトを検索し、見つからなければ、その[[プロトタイプ]]に進みます。それでも[[プロトタイプ]]でプロパティにマッチするものがない場合、JavaScriptは、チェーン内のすべてのオブジェクトが検索の対象となるまで、リンクされたオブジェクトのプロトタイプからチェックを試みます。各チェーンの最後にはObject.prototypeが含まれており、Object.prototypeからメソッドやプロパティが継承されています。チェーンを越えて検索すると、常にnull出力が返されます。上の例では、Object からの継承を持つ空のオブジェクト x があります。もし Object が toString() のような関連するプロパティやメソッドを持っていれば、それらをすべて x に使用することができます。

x.toString();
Output
[object Object]

これは、x -> Objectと定義できそうなリンクが1つだけあるプロトタイプチェーンです。他のリンクがあるかどうかは、[[Prototype]]プロパティを2回チェーンすることで確認できます。他のチェーンがない場合、出力はnullです。

x.__proto__.__proto__;
Output
null

では、今回は別のタイプのオブジェクトを探っていきます。具体的には、すでにJavaScriptの配列に遭遇したことがある人は、他の組み込みメソッドであるpop()とpush()に慣れているはずです。この2つのメソッドは、新しい配列が作成されたときに常にアクセス可能です。その理由は、新しく作成されたすべての配列がArray.prototypeに固有のアクセシビリティを持っているからです。以下の行を使って、新しい配列を作成してみましょう。

let y = [];

配列コンストラクタを使用して新しい配列を作成する方法もあります。

let y = new Array()

このように新しい配列の[[プロトタイプ]]を調べてみると、Array.prototypeに関連付けられたすべてのプロパティを継承しているので、xオブジェクトに比べてはるかに多くのプロパティとメソッドを含んでいることがわかります。

y.__proto__;
[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]

私たちの配列にはプロトタイプのコンストラクタプロパティがありますが、これは鮮やかにArray()に設定されています。これは、オブジェクトのコンストラクタ関数を返すというもので、オブジェクトを構築するために関数を使うことができる仕組みです。

さらに、私たちの長いチェーンの中の2つのプロトタイプを次のような構文にチェーンすることができます: y -> Array -> Object。

Array.prototypeを調べると、以下のようになります。

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 は、以下に示すように、プロトタイプ チェーン内のコンストラクタのプロトタイプ プロパティの外観をテストするために使用できるメソッドです。

y instanceof Array; // true

要約すると、すべてのJavaScriptオブジェクトに隠された内部の[[Prototype]]プロパティがあることがわかりました。このようなプロパティはprotoメソッドを使ってアクセスできます。私たちは、コンストラクタの[[Prototype]]プロパティとメソッドを継承することでオブジェクトを拡張することができるという基本的な事実のために、オブジェクトを拡張することができます。チェーン化されると、チェーンに追加されたすべてのオブジェクトは、チェーンの最後までプロパティとメソッドを継承し、Object.prototypeで終了します。

コンストラクタ関数

基本的にコンストラクタ関数は、新しいJavaScriptオブジェクトを作成します。コンストラクタ関数をベースにした新しいインスタンスを起動するには、new演算子を使用します。しかし、JavaScript はまた、新しい Array() と新しい Date() のような組み込みのコンストラクタが含まれていますが、我々 を使用してオブジェクトを作成する新しいものを作成することも可能です。

たとえば、テキストに基づいている単純なロールプレイングゲームを作成しようとしている可能性があります。このゲームでは、プレイヤーはキャラクターと、医師、兵士、強盗などの関連するクラスを選択しなければなりません。

全てのキャラクターは、名前、レベル、ポイントなどの共通点を持っているので、キャラクターのコンストラクタは共通であることが望ましい。同時に、各キャラクターは、キャラクターごとに異なる能力にしかアクセスできないように制限されるものとする。この目的を達成するために、プロトタイプの継承とコンストラクタ関数を使用することになります。

まず、コンストラクタ関数が他の通常の関数と同じであることに注意してください。2つの間の重要な差別化は、コンストラクタは新しいキーワードを持つインスタンスによって呼び出されるということです。コンストラクタ関数の最初の文字を大文字にすることが JavaScript の要件です。例を参照してください。

// Initialize a constructor function for a new Actor
function Actor(name, level) {
  this.name = name;
  this.level = level;
}

この例では、name と level というパラメータで構成されるコンストラクタ関数 (Actor) を作成しています。テキスト・ゲームのすべてのキャラクタには名前と関連するレベルがあり、それらすべてが上記のコンストラクタ関数に含まれるプロパティを共有するのが理想的です。キーワード this は、作成された新しいインスタンスを示します。したがって、すべてのオブジェクトが name プロパティを持つようにするには、例に示したように this.name を name パラメータにも設定する必要があります。

では、new.nameを使って新しいインスタンスを構築してみましょう。

let actor1 = new Actor('Bjorn', 1);

では、actor1をコンソールしてみて、新しいオブジェクトを作成したかどうか、そのプロパティはどうなるのかを観察してみましょう。

Output
Actor {name: "Bjorn", level: 1}

さらに、コンストラクタをActor()として出力するはずのactor1の[[Prototype]]を調べてみます。

Object.getPrototypeOf(actor1);
Output
constructor: ƒ Actor(name, level)

act1.proto メソッドでも同じ結果が得られますが、前述したようにブラウザとの互換性が限られているため、あまり適切ではないことを覚えておいてください。

コンストラクタにはプロパティしか定義されていないので、まだメソッドを定義していません。JavaScriptでは、効率性とコードの可読性を向上させるために、プロトタイプのメソッドを定義する必要があります。

そこで、今回はプロトタイプを使ってActorのメソッドを定義してみましょう。今回はgreet()メソッドを使ってデモを行います。

characterSelect.js

// Add greet method to the Actor prototype
Actor.prototype.greet = function () {
  return `${this.name} says hello.`;
}

Actorはプロトタイプの一部としてgreet()メソッドを持つようになりました。したがって、hero1はActorのインスタンスを構成しているので、actor1も同様にメソッドを継承しなければなりません。

Actor1.greet();
Output    
"Bjorn says hello."

必要であれば、アクターの[[プロトタイプ]]を検査してみると、greet()が利用できるようになっていることがわかります。しかし、アクターのためにキャラクタークラスを作成する必要があります。先に述べたように、ゲームのアクターは異なる能力を持っています。そのため、すべてのキャラクターの能力をアクターのコンストラクタに含めるのは非論理的です。この問題にどのようにアプローチするかというと、オリジナルのアクターに接続されたままの新しいコンストラクタ関数を作成します。

次に、あるコンストラクタのプロパティを別のコンストラクタにエクスポートするために call() メソッドを使用します。兵士と医師のコンストラクタを作成してみましょう。

// Initialize Soldier constructor
function Soldier(name, level, weapon) {
  // Chain constructor with call
  Actor.call(this, name, level);

  // Add a new property
  this.weapon = weapon;
}

// Initialize Doctor constructor
function Doctor(name, level, spell) {
  Doctor.call(this, name, level);

  this.spell = spell;
}

ご覧のように、アクターのプロパティを含むコンストラクタを作成しました。ここで、SoldierとDoctorに新しいメソッドを追加します。attack()メソッドはSoldierに、heal()メソッドはDoctorに適用されます。

Soldier.prototype.attack = function () {
  return `${this.name} attacks with the ${this.weapon}.`;
}

Doctor.prototype.heal = function () {
  return `${this.name} casts ${this.spell}.`;
}

2つのクラスをキャラクターに含めるように進めていきましょう。

characterSelect.js
const actor1 = new Soldier('Bjorn', 1, 'axe');
const actor2 = new Doctor('Kanin', 1, 'cure');

何が効果的かというと、以下のようになります。

act1を兵士として認識し、追加されたプロパティを認識します。

Soldier {name: "Bjorn", level: 1, weapon: "axe"}

ここまで紹介してきたメソッドをSoldierのプロトタイプで使ってみましょう。

actor1.attack();
Console
"Bjorn attacks with the axe."

しかし、2つ目の方法で試してみると、以下のような結果が得られます。

actor1.greet();
Output
Uncaught TypeError: actor1.greet is not a function

明らかに、新しいメソッドはcall()では自動的に継承されません。プロトタイプをリンクするために必要なのは、Object.create()を使用することです。ただし、プロトタイプのために作成したメソッドの前に配置する必要があります。

Soldier.prototype = Object.create(Actor.prototype);
Doctor.prototype = Object.create(Actor.prototype);

// We add all other prototype methods below

ソルジャーやドクターのインスタンスにアクターのプロトタイプメソッドが追加されました。

Doctor.
actor1.greet();
Output
"Bjorn says hello."

以下は、キャラクター作成のための全体のコードです。

// Initialize constructor functions
function Actor(name, level) {
  this.name = name;
  this.level = level;
}

function Soldier(name, level, weapon) {
  Actor.call(this, name, level);

  this.weapon = weapon;
}

function Doctor(name, level, spell) {
  Actor.call(this, name, level);

  this.spell = spell;
}

// Link prototypes and add prototype methods
Soldier.prototype = Object.create(Actor.prototype);
Doctor.prototype = Object.create(Actor.prototype);

Actor.prototype.greet = function () {
  return `${this.name} says hello.`;
}

Soldier.prototype.attack = function () {
  return `${this.name} attacks with the ${this.weapon}.`;
}

Doctor.prototype.heal = function () {
  return `${this.name} casts ${this.spell}.`;
}

// Initialize individual character instances
const actor1 = new Soldier('Bjorn', 1, 'axe');
const actor2 = new Doctor('Kanin', 1, 'cure');

このチュートリアルで達成したことは、プロトタイプと継承の概念を実証することです。ベースとなるプロパティを持つ Actor クラスの作成に成功しました。次に、最初のコンストラクタを継承して、SoldierとDoctorのキャラクタークラスを作成しました。最後に、プロトタイプに新しいメソッドを追加し、各キャラクターのインスタンスを作成しました。

まとめ

従来のクラスベースのプログラミング言語とは異なり、JavaScriptはプロトタイプに基づいています。私たちはそのプロトタイプを探求し、[[Prototype]]プロパティを使ってオブジェクトをチェーンにつなげる方法を学びました。また、チェーンを介してプロトタイプのプロパティを継承してきました。

Alibaba Cloudのアカウントをお持ちでない方は、アカウントを登録してください。アカウントにサインアップして、最大1200ドル相当の40以上の製品を無料でお試しください。Alibaba Cloudの詳細については、「Get Started with Alibaba Cloud」を参照してください。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

3
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3