LoginSignup
8
3

More than 3 years have passed since last update.

プロトタイプベースのオブジェクト指向とJavaScriptのプロトタイプ

Last updated at Posted at 2019-12-24

社内勉強会の資料

概要

JSのプロトタイプについての理解と、その周辺の登場人物について理解する回。

もう過去何億人がこの話題について人に説明したかわからない程にはjsの基本のキですが、自分なりの解釈と自分なりの説明を書いてみます。

ここでの説明が理解できれば、MDNのプロトタイプに関する記述はすんなり読めるようになります。

メインの対象者としては、「jsは普通に書いてるしプロトタイプもなんとなく知ってるけど、説明しろって言われるとちょっと言葉に詰まる」という方です。

登場人物(tl;dr)

  • コンストラクタ
    • new演算しと共に使用される、オブジェクトを生成するための処理を書いた関数。
  • .prototype
    • コンストラクタオブジェクトが持つ、コンストラクタ(自身)のプロトタイプへの参照を持つプロパティ。
  • [[prototype]]
    • ECMAScriptの仕様において、インスタンスのプロトタイプへの参照を持つ内部プロパティ。
    • privateなプロパティなので、jsコード上で参照することは出来ない。
  • .__proto__
    • ECMAScriptには元々定義されていなかったが、ChromeやFireFoxなどには実装されている。
    • インスタンスに存在する、コンストラクタのプロトタイプへの参照を持つプロパティ。
    • [[prototype]]と同じだが、こちらはjsコード上からアクセス可能。
    • ES6から初めて仕様として定義されたが、同時に非推奨となっている。
  • Object.getPrototypeOf()
    • オブジェクトのプロトタイプを取得する関数。
    • ES6から正式に仕様に存在し、__proto__に代わって推奨されるプロトタイプへのアクセス方法。
  • Object.setPrototypeOf()
    • オブジェクトのプロトタイプを代入する関数。
    • ES6から正式に仕様に存在し、__proto__に代わって推奨されるプロトタイプへのアクセス方法。
  • new
    • コンストラクタから新しいオブジェクトを作る演算子。
  • class
    • プロトタイプベース継承のシンタックスシュガー。
    • クラスベースのオブジェクト指向におけるクラスとは本質的には別物。

流れ

説明の流れとしては、

  1. プロトタイプベースのオブジェクト指向の概念の理解
  2. プロトタイプをふんわりとコードで理解
  3. プロトタイプを実際のコードで理解

という感じで進めます。

プロトタイプベースのオブジェクト指向って?

オブジェクト指向 - wiki
The Early History Of Smalltalk

1, EverythingIsAnObject.
2, Objects communicate by sending and receiving messages (in terms of objects).
3, Objects have their own memory (in terms of objects).
4, Every object is an instance of a class (which must be an object).
5, The class holds the shared behavior for its instances (in the form of objects in a program list).
6, To eval a program list, control is passed to the first object and the remainder is treated as its message.
— Alan Kay

1, すべてはオブジェクトである。
2, オブジェクトはメッセージの受け答えによってコミュニケーションする。
3, オブジェクトは自身のメモリーを持つ。
4, どのオブジェクトもクラスのインスタンスであり、クラスもまたオブジェクトである。
5, クラスはその全インスタンスの為の共有動作を持つ。インスタンスはプログラムにおけるオブジェクトの形態である。
6, プログラム実行時は、制御は最初のオブジェクトに渡され、残りはそのメッセージとして扱われる。

プロトタイプベース - wiki

全てがクラスベースであるという前提は時に物事を複雑化してしまう。

例えばメソッドが必ず何らかのクラスに所属するという前提は強すぎる場合がある。クラスベースでは委譲や代理(プロキシ)によって動作にバリエーションを与えるが、初めからバリエーションをもったインスタンスを作成できればそのような機構は必要ない。

またインスタンス変数とメソッドの違いとは何か、という問題もある。C++やJavaのpublicなメンバ変数(フィールド)などを別にすれば、インスタンス変数とアクセサメソッドはほとんど等価の概念である。

クラスが存在しない世界ではテンプレート処理によるインスタンス化ができないため、新しいオブジェクトは完全に空の状態か、または他のオブジェクトの複製(クローン)によって作成される。プロトタイプベースでの継承は一般にこのクローンによる特性の引き継ぎを指す。

プロトタイプベースのオブジェクト指向とは、クラスベースのオブジェクト指向とは違ったアプローチでオブジェクト指向を実装したもの。

クラスベースのオブジェクト指向は、クラスという共通の挙動を定義したオブジェクトをテンプレートとして、その挙動を共通に持つ新しいオブジェクトを複数作ることができる。
それに対して、プロトタイプベースのオブジェクト指向にはクラスが存在せず、プロトタイプと呼ばれる共通の挙動を格納したオブジェクトへの参照を持つことで、オブジェクトは共通の挙動を得る。

(この辺は言語によって細かい思想が違うので、一概に同じ言葉で説明出来ない部分もあると思うけど、概念的にはこう)

つまり、どちらにせよ

1, すべてはオブジェクトである。
2, オブジェクトはメッセージの受け答えによってコミュニケーションする。
3, オブジェクトは自身のメモリーを持つ。

これらの思想は受け継いだうえで、オブジェクト間で共通の挙動を、どう定義するかの思想が異なっている。

つまり、プロトタイプベースのオブジェクト指向とは、「複数のオブジェクトに共通の挙動を持たせる方法として、クラスを使用せず、プロトタイプを使用するオブジェクト指向」のこと。

改めてプロトタイプ

したがって、プロトタイプとは、クラスとは違う方法で、複数のオブジェクトに共通の挙動を与える仕組みのこと。

prototypeとは - Weblio

主な意味
原型、模範、原形

プロトタイプと聞くと、「何かを作る際にとりあえず作るもの」というようなイメージが日本語的には強いですが、プロトタイプベースのオブジェクト指向における「プロトタイプ」は「原型」という意味で捉える方がしっくり来ます。「ひな形」「テンプレート」とも言えるでしょう。

オブジェクト指向において、オブジェクトに対して共通の振る舞いを持たせることを「継承」と呼びますが、これはプロトタイプベースであっても同じです。

jsにおけるプロトタイプの実装

ここまではプロトタイプベースのオブジェクト指向の概念でした。
ここからは、具体的にjsにおけるプロトタイプベースのオブジェクト指向の実装を見ていきます。
どうやって、プロトタイプが複数のオブジェクトに共通の挙動を持たせているか、という話です。

複数のオブジェクトに共通の挙動を持たせる方法を雰囲気で理解

さて、jsにはクラスが存在しませんが、複数のオブジェクトに共通の挙動を持たせたい場面はプログラミングをしていると頻出します。
ここでは、わかりやすいようにプロトタイプの実装をプロトタイプを使わずにふんわり再現してみようと思います。
ここのコードは雰囲気なので実際には意図した挙動にならないものを含んでいます。
雰囲気を感じ取ってください()

例えば、5つの異なるオブジェクトが存在するとして、

const obj1 = { hoge: "HOGE" };
const obj2 = { piyo: "PIYO" };
const obj3 = { num: 123 };
const obj4 = { fuga: "fuga" };
const obj5 = { f: () => {} };

このオブジェクトたちを、文字列に変えたいとしましょう。
ログに吐きたいんです。名前は、toStringとでもしましょう。

全部に同じ処理を持たせようと思ったら、

const toString = () => {
  // 文字列化する処理
};

obj1.toString = toString;
obj2.toString = toString;
obj3.toString = toString;
obj4.toString = toString;
obj5.toString = toString;

こんな感じでどうでしょう。

あれ、オブジェクトをJSON形式にする、toJSONも入れたいですか?
一個ずつ代入しても良いですが、数が増えるとツラそうなので、一つのオブジェクトにして、全部にそれを渡しますか。

const commonFunctons = {
  toString: () => { /* 文字列化する処理 */ },
  toJSON: () => { /* JSON形式に変換する処理 */ },
};

// 変更されたくないので、触んないでね!という意味で、両はしをアンダーバー二つとかで囲っときましょう
obj1.__commonFunctions__ = commonFunctions;
obj2.__commonFunctions__ = commonFunctions;
obj3.__commonFunctions__ = commonFunctions;
obj4.__commonFunctions__ = commonFunctions;
obj5.__commonFunctions__ = commonFunctions;

こうすれば、複数のオブジェクトに同じ挙動を持たせることが出来そうですね。
プロトタイプの基本的な方針は、こういう感じです。

ここで言うcommonFunctionsがプロトタイプと呼ばれます。
共通の挙動をするオブジェクトたちの「原型」です。

ちなみにこれ、呼び出す時に

obj5.__commonFunctions__.toString();

こうなりますが、うーん、

obj5.toString();

こう呼び出したいですよね。
__commonFunctions__は、もうルールとして、省略できるということにしましょう。

この、呼び出す際にプロトタイプの参照を省略して良い仕組みを、プロトタイプチェーンと呼びます。

__commonFunctions__は、実際のjsで言う__proto__に相当します。
__proto__は、そのインスタンスオブジェクトが参照しているプロトタイプへの参照です。
上の例では、commonFunctionsオブジェクトへの参照ですね。

__proto__はブラウザなどが勝手に実装しているプロパティでしたが、ES6から初めて仕様として定義されました。
が、同時に非推奨となっています。
(推奨の方法としてgetPrototypeOfsetPrototypeOfが正式に定義されましたが、このページでは解説のしやすさと理解しやすさから__proto__を使います。)

実際のprototypeの仕様

雰囲気を掴んだところで、本当のjsのprototypeの仕様を見ていきます。

ちなみに、関数もオブジェクト

jsはオブジェクト指向なので、だいたいのものがオブジェクトです。
さらに、jsでは関数が第1級オブジェクトなので、関数もオブジェクトです。
つまり、関数自体もプロパティを持ちます。
これを認識しておかないとプロトタイプの話においては結構混乱します。

function doSomething() {
  console.log("something");
}

doSomething.hoge = "hoge"; // 問題なく通る

プロトタイプの確認

さて、わかりやすいように、具体的なオブジェクトを使いましょう。

const date1 = new Date();
const date2 = new Date();
const date3 = new Date();

Dateを使って新たにオブジェクトを3つ作りました。

先ほどの例で言えば、この3つのdateオブジェクトは、共通の挙動を持った一つのオブジェクト(何度も言いますが、これをプロトタイプと呼びます)(上の例で言うとcommonFunctonsオブジェクト)を参照しているはずです。
それはChromeなどのブラウザであれば、

date1.__proto__
date2.__proto__
date3.__proto__

で確認できます。

スクリーンショット 2019-12-20 22.33.45.png

これは上一部しか表示してませんが、Dateオブジェクトでよく使う関数がオブジェクトの中に入っているのがわかりますよね。上の例で言うcommonFunctionsそのものの参照が取得出来ているのがわかると思います。

さらに、上の例では、全てのオブジェクトに、同じcommonFunctionsオブジェクトの参照を入れてましたよね?
なので、

date1.__proto__ === date2.__proto__ // true
date2.__proto__ === date3.__proto__ // true
date1.__proto__ === date3.__proto__ // true

こうなります。つまり、どのDateオブジェクトも、全て全く同じプロトタイプへの参照(厳密等価演算子===による比較で判断できる)を保持しています。

プロトタイプ本体はどこに?

では、Dateオブジェクトがみんな参照している一つのプロトタイプは、実際どこにあるのでしょうか。
それは、Date関数=Dateコンストラクタが所持しています。

コンストラクタが所持しているプロトタイプは、.prototypeでアクセス出来ます。

スクリーンショット 2019-12-20 22.39.16.png

Date.prototypeによって得られたオブジェクトは、date1.__proto__で得られたオブジェクトと全く同じです。

// 厳密等価演算子でtrueが返る
Date.prototype === date1.__proto__ // true

つまりjsでは、オブジェクトの共通の挙動として、自身のコンストラクタのプロトタイプを参照します。

プロトタイプチェーン

さて、プロトタイプは発見できたので、プロトタイプ上にある関数を実行してみましょう。

date1.getFullYear(); // 2019

2019が返ってきました。
でもこれ、よく考えると変ですよね?
getFullYearを所持しているのはdate1ではなく、Dateオブジェクトのプロトタイプ(date1.__proto__であり、Date.prototype)のはずです。

つまり、

date1.__proto__.getFullYear();

って呼び出すべきじゃない?

そこで登場するのがjsのプロトタイプチェーンという仕組みです。
jsでは、とあるオブジェクトに対して存在しないプロパティが参照された場合、そのオブジェクトのプロトタイプに同名のプロパティがあるかどうか探しに行きます。
そして見つかれば、そのプロパティを返します。

つまりdate1で言えば、date1.getFullYearが呼び出された時点で、まずdate1オブジェクトの中にgetFullYearプロパティが存在するかどうかをチェックします。もちろん、代入していないのでありません。
次に、date1__proto__の中を探します。ここにはありました!ということで、Date.prototype.getFullYearを返します。

この文字面、どこかで見覚えありませんか?
そう、MDNで調べるとよく出てくる記述です。

スクリーンショット 2019-12-20 23.27.33.png

これこれ!

じゃあ、逆に言えば、date1オブジェクトにgetFullYearプロパティがあったら、そっち返すっていうことだよね?

const date1 = new Date();
date1.getFullYear = 123; // 問題なく通る

結果は...

date1.getFullYear(); // 123

123が返ってきました。
先ほど書いたプロトタイプチェーンの仕様通りですね。

しかし、ここまでは「チェーン」って言うほどチェーンしてません。
ということでここから、継承の話に移ります。

継承

jsで言う継承とは、プロトタイプの繋がりを指します。

プロトタイプチェーンは、そのオブジェクトのプロトタイプに該当するプロパティが存在しなかった場合、プロトタイプのプロトタイプに探しに行きます。
そしてそこにも無い場合はプロトタイプのプロトタイプのプロトタイプに...というように、プロトタイプをどんどん遡って行き、最終的にプロトタイプがnullを返すまでこのチェーンは続きます。

例えば、date1のプロトタイプをたどると、以下のようになります。

date1.__proto__                      === Date.prototype   // true
date1.__proto__.__proto__            === Object.prototype // true
date1.__proto__.__proto__.__proto__  === null             // true

つまりここでは、Object > Date > date1という継承関係になっていると言えます。

クラスベースで言うなら、

class Object {
}

class Date extends Object {
}

Date date1 = new Date();

という感じですね。

複数回のプロトタイプチェーン

先ほどのdate1オブジェクトからは、hasOwnProperty関数が実行できます。

date1.hoge = "HOGE";
date1.hasOwnProperty("hoge"); // true

この関数は「継承由来ではない特定のプロパティを持っているかどうか調べる関数」ですが、下記のように、Date.prototypeにはhasOwnProperty関数は存在していません。

// `Date.prototype`に、`"hasOwnProperty"`という独自のプロパティが存在するかどうかを確認し、`false`が返っています(つまり持っていない)。
Date.prototype.hasOwnProperty("hasOwnProperty") // false

ではこのhasOwnPropertyは誰が持っているかというと、Object.prototype.hasOwnPropertyが持っています。

Object.prototype.hasOwnProperty("hasOwnProperty") // true

つまり、date1に対してhasOwnPropertyプロパティが呼び出された場合、Date.prototypeを探して発見できず、さらにそのプロトタイプ(Object.prototype)を探してそこで発見し、Object.prototype.hasOwnPropertyを返します。

プロトタイプとしてnullが返ってくるまで見つからなかった場合、jsは返り値としてundefinedを返します。

プロトタイプチェーンの終端の実験

プロトタイプチェーンの終端がnullだということは、自分でnullを代入したらプロトタイプチェーンは止まるんでしょうか、やってみましょう。

date1.__proto__.__proto__ = null

これで、本来であればObject.prototypeを参照しているはずのdate1.__proto__.__proto__nullになります。この状態で、Object.prototypeに存在するプロパティを呼び出すと...

date1.hasOwnProperty("hasOwnProperty")
Uncaught TypeError: date1.hasOwnProperty is not a function
    at <anonymous>:1:7

関数じゃないって🤔
じゃあ...

date1.hasOwnProperty // undefined

undefinedでした。

というように、自身に存在していないプロパティでも、プロトタイプを再帰的に遡って該当するプロパティを引っ張ってくる仕組みが、プロトタイプチェーンです。

そして、このプロトタイプチェーンで繋がる関係を継承と呼びます。

コンストラクタ

さて、先ほどから普通に使用していますが、コンストラクタとはなんでしょうか。
jsにはクラスは存在しませんが、コンストラクタは存在します。

コンストラクタ関数の使用 - MDN

jsにはオブジェクトを作る方法が2種類存在し、それが純粋なオブジェクト生成(オブジェクトリテラルや、new Object()など)と、new演算子を使用したオブジェクト生成です。

後者のnewを使う方法は、関数と共に記述します。JSにおいて、このnewと共に使用される関数のことを、コンストラクタと呼びます。

// `Date`がコンストラクタ
const date = new Date();

コンストラクタとなる関数の条件は、アロー関数ではないこと、のみです。

つまり、先ほどのdoSomethingという関数もnewをつければコンストラクタになります。

const instance = new doSomething(); // 問題なく通る

コンストラクタ関数はアッパーキャメルケース(UpperCamelCase)で書く慣習がありますが、上記のように文法レベルでの制約はありません。

クラスベースのオブジェクト指向に慣れているとnew Date()を見たときに「Dateクラスのインスタンスを作っているな」と見えるのですが、これはプロトタイプベースな目線で見ると、「Date関数をコンストラクタとしてインスタンスオブジェクトを作っている」と言えるでしょう。
(ただ、ES6でclass構文が出来てからは、ここで言うコンストラクタをクラスと呼ぶようにもなりました。ので、実際のところ「Dateクラスのインスタンス」と言うのも正しいと思います。実際自分で書く際はclass構文の方が楽だし読みやすい...ただシンタックスシュガーであることに変わりはないので、実際に行われているのはプロトタイプを用いたオブジェクト生成です)

newのお仕事

では、通常のオブジェクト作成とnewによるオブジェクト作成は何が違うんでしょうか。

new 演算子 - MDN

  1. 空の JavaScript オブジェクトを生成する
  2. このオブジェクト (のコンストラクター) を他のオブジェクトへリンクする
  3. ステップ 1 で新しく生成されたオブジェクトを this コンテキストとして渡す
  4. 関数が自分自身を返さない場合は this を返す

注目すべきは

  1. このオブジェクト (のコンストラクター) を他のオブジェクトへリンクする

です。
ここだけ曖昧な表現になっていてよくわかりませんが、具体的に意識するべきなのは以下の2点です。

  1. 新規作成したオブジェクトのconstructorプロパティにコンストラクタ関数自体の参照を代入する。
  2. 新規作成したオブジェクトの[[prototype]]にコンストラクタ関数のプロトタイプの参照を代入する。

jsのES5までの仕様では、インスタンスオブジェクトのプロトタイプへの直接の書き込み権限がプログラマにありません([[prototype]]がプライベートであり、かつ__proto__は仕様に存在しなかったため)。

つまり、newを使ってのみ、プロトタイプ継承を用いたオブジェクト生成をすることができます。

jsにおけるnewとは、継承されたオブジェクトを作成するベストな方法であると言えるでしょう。

class構文とプロトタイプ継承

TODO: 同じものを書く場合の比較とかを頑張って書く

オートボクシング

TODO: プリミティブ値とプロトタイプの話を頑張って書く

プロトタイプ汚染

TODO: 頑張って書く

おしまい

ということで、現状まだ書いてないことがいっぱいあるのですが、プロトタイプの最低限を書いたので一旦終わりとします。
続きはまたどこかで頑張って書きます。

8
3
0

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
8
3