JavaScript プロトタイプ継承の仕組みをGIFアニメで分かりやすく解説

JavaScriptにはどのオブジェクトにもプロトタイプと呼ばれる便利な隠しプロパティがあります。プロトタイプ継承の仕組みについてGIFアニメーションで分かりやすく解説された記事を紹介します。

JavaScript プロトタイプ継承の仕組みをGIFアニメで分かりやすく解説

🎉👨‍👩‍👧‍👧 JavaScript Visualized: Prototypal Inheritance
by Lydia Hallie

下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。

JavaScriptを視覚的に解説: プロトタイプ継承

文字列や配列、オブジェクトに対して.length, .split(), .join()のような組み込みメソッドを使用できるのか、疑問に思ったことはありませんか? それらを明示的に指定したことはありませんが、どこからきたものでしょうか? これは、プロトタイプ継承(prototypal inheritance)と呼ばれるものが原因です。プロトタイプ継承はかなり便利で、あなたは意外と気がつかずに頻繁に使用していると思います!

私は視覚学習者なので、GIFアニメで視覚的に説明することで、あなたの手助けをしようと思います。

わたし達は多くの場合、同じタイプの多くのオブジェクトを作成する必要があります。例えば、犬を閲覧できるサイトがあるとしましょう。

すべての犬🐕にその犬をあらわすオブジェクトが必要です。毎回新しいオブジェクトを作成する代わりに、newキーワードを使用してDogインスタンスを作成できるコンストラクタ関数(ES6のclassについては後ほど説明)を使用します(この記事ではコンストラクタ関数についてはあまり説明しないので、ここでは触れません)。

すべての犬には名前(name)・品種(breed)・色(color)、そして鳴き声(bark)があります。

犬をあらわすオブジェクト

犬をあらわすオブジェクト

Dogコンストラクタ関数を作成したとき、作成されるオブジェクトはDogだけではありません。自動的にプロトタイプ(prototype)と呼ばれる別のオブジェクトも作成されます。デフォルトで、このオブジェクトにはconstructorプロパティが含まれており、元のコンストラクタ関数(この場合はDog)を参照できます。

コンストラクタ関数(Dog)を作成すると、prototypeオブジェクトも作成される。constructorプロパティは、元のコンストラクタ関数への参照が可能。

Dogコンストラクタ関数のprototypeプロパティは列挙できないため、オブジェクトプロパティにアクセスしても表示はされません。しかし、存在はしてします!

このプロパティオブジェクトがあるのはなぜでしょうか?
最初に、表示したい犬をいくつか用意してみます。分かりやすくするために、ここではdog1, dog2にしました。dog1はブラックのかわいいラブラドールで名前はDaisy、dog2はホワイトのジャックラッセルテリアで名前はJackです😎

2匹の犬を定義

2匹の犬を定義

dog1をコンソールに記録し、プロパティを展開してみました。

dog1のログ

名前(name)・品種(breed)・色(color)、鳴き声(bark)と、定義したプロパティが表示されましたが、最後に表示された__proto__プロパティとは何でしょう?
これは展開されません。つまり、オブジェクトのプロパティを取得しようとしても、通常は表示されません。展開して、プロパティを見てましょう😃

__proto__を展開

Dog.prototypeオブジェクトとまったく同じです!
つまり、__proto__はDog.prototypeオブジェクトへの参照です。これがプロトタイプ継承です。コンストラクタの各インスタンスは、コンストラクタのプロトタイプにアクセスできます!🤯

インスタンスには__proto__という名前のプロパティも含まれます。これはコンストラクタのプロトタイプ、この場合はDog.prototypeへの参照です。

では、なぜこれがクールなのでしょうか?
すべてのインスタンスが共有するプロパティがある場合もあります。例えば、この場合の関数barkはすべてのインスタンスでまったく同じですが、新しい犬を作成するたびに新しい関数を作成し、そのたびにメモリを消費するのはなぜでしょうか。その代わり、わたし達はDog.prototypeオブジェクトに追加することができます!🥳

毎回プロパティのコピーを新しく作成する代わりに、すべてのインスタンスが共有できるプロパティをプロトタイプに追加することで、メモリを節約できます。

インスタンスのプロパティにアクセスしようとするたびに、エンジンはまずローカルで検索して、そのプロパティがオブジェクト自体で定義されているかどうかを確認します。ただし、アクセスしようとしているプロパティが見つからない場合、エンジンは__proto__プロパティを介してプロトタイプチェーンをたどります。

オブジェクトのプロパティにアクセスしようとすると、最初にローカルで検索され、次に__proto_-プロパティを介してプロトタイプチェーンをたどる。

上記は1つのステップにすぎませんが、複数のステップを含めることもできます!Dog.prototypeを表示し、__proto__オブジェクトを展開したときに、1つのプロパティも含まれていないことに気がついたかもしれません。Dog.prototype自体は、オブジェクトです。つまり、実際にはObjectコンストラクタのインスタンスです。つまり、Dog.prototypeには、Object.prototypeへの参照である__proto__プロパティも含まれています!

__proto__プロパティを展開

最後に、すべての組み込みメソッドがどこから来たのかという答えがあります。
プロトタイプチェーンです!😃

例えば、.toString()メソッド。dog1オブジェクトでローカルに定義されていますか? いいえ。では、dog1.__proto__にオブジェクトで定義されていますか、つまりDog.prototypeへの参照がありますか? それもいいえです。
Dog.prototype.__proto__がObject.prototypeへの参照を持つオブジェクトで定義されていますか? はい!🙌🏼

プロトタイプチェーンにはいくつかのステップがあります。例えば、Dog.prototype自体はオブジェクトであるため、組み込みのObject.prototypeからプロパティを継承します。

コンストラクタ関数(function Dog() { ... })を使用していますが、これはまだ有効なJavaScriptです。しかし、ES6ではコンストラクタ関数のより簡単な構文が導入され、プロトタイプを操作できます!

classは、コンストラクタ関数の糖衣構文(読み書きのしやすさのために導入された書き方)です。機能は同じです!

classキーワードを使用してクラスを記述します。classにはconstructor関数があり、これは基本的にはES5で記述したコンストラクタ関数と同じ機能です。プロトタイプに追加するプロパティは、classで定義します。

ES6はclassを導入しました。これはコンストラクタ関数の糖衣構文です。

classのもう1つの素晴らしい点は、他のclassを簡単に拡張できることです。

例えば、同じ品種の犬、チワワを何匹か見せたいとします。チワワは(どういうわけか...😐)まだ犬です。この例を分かりやすくするために、ここではname, breed, colorの代わりに、nameプロパティだけをDogクラスに渡します。しかし、このチワワは特別なこともできます。チワワは小さな鳴き声を持っています。「Woof!」と鳴く代わりに、「Small woof!」と鳴くこともできます🐕

拡張したclassでは、superキーワードを使用して親のclassのコンストラクタにアクセスできます。親classのコンストラクタが必要とする引数、この場合はsuper:nameに渡す必要があります。

拡張したclass

拡張したclass

myPetは、Chihuahua.prototypeとDog.prototypeの両方にアクセスできます(Dog.prototypeはオブジェクトなので、Object.prototypeに自動的にアクセスします)。

プロトタイプ継承は、ES5コンストラクタと同じようにclassで機能します。superキーワードを使用すると、サブクラスが拡張するクラスを呼び出すことができます。

Chihuahua.prototypeにはsmallBark関数があり、Dog.prototypeにはbark関数があるため、myPetでsmallBarkとbarkの両方にアクセスできます!

あなたの想像通り、プロトタイプチェーンは永遠に続くわけではありません。最終的に、プロトタイプがnullに等しいオブジェクト、この場合はObject.prototypeオブジェクトです。ローカルまたはプロトタイプチェーンで見つからないプロパティにアクセスしようとすると、undefinedが返されます。

拡張classから継承されたメソッドを呼び出すことができます。プロトタイプチェーンは__proto__の値がnullの場合に終了します。

この記事では、コンストラクタ関数とclassを使って説明しましたが、オブジェクトにプロトタイプを追加する別の方法は、object.createメソッドを使用することです。このメソッドでは新しいオブジェクトを作成し、そのオブジェクトのプロトタイプを正確に指定できます💪🏼

そのためには、既存のオブジェクトを引数としてObject.createメソッドに渡します。このオブジェクトが、作成するオブジェクトのプロトタイプです!

既存のオブジェクトを引数としてObject.createに渡す

既存のオブジェクトを引数としてObject.createに渡す

作成したmeオブジェクトをログに記録してみます。

meオブジェクトのログ

meオブジェクトにプロパティを追加したわけではありません。単に列挙できない__proto__プロパティが含まれているだけです。__proto__プロパティは、プロトタイプとして定義したオブジェクトへの参照が保持されています。つまり、nameとageプロパティを持つpersonオブジェクトへの参照があります。personオブジェクトはオブジェクトなので、personオブジェクトの__proto__プロパティの値はObject.prototypeです(読みやすくするために、GIFアニメではこのプロパティを展開しませんでした)。

これでJavaScriptの素晴らしい世界で、プロトタイプ継承が重要な機能である理由を理解できたと思います。
質問があれば、気軽にお問い合わせください😊

sponsors

top of page

©2020 coliss