前の記事でオブジェクト指向にはざっくり2つの考え方があると書いた。それとは別にオブジェクト指向を実現するためのアプローチとしてプロトタイプベースのオブジェクト指向がある。
オブジェクト指向には2つのアプローチがある
まずオブジェクト指向にはざっくり2つのアプローチがある。
そのうちの一つがクラスベースである。こちらは言わずもがな、クラスをもとにインスタンスを生成することでオブジェクト指向を実現する手法である。実はSmalltalkもC++も、考え方は違うものの、同じクラスベースである。
では、プロトタイプベースのオブジェクト指向はご存じだっただろうか?このプロトタイプベース、実はとても有名なプログラミング言語に採用しているとされている。
それは、JavaScriptである。
JavaScriptは、奇妙なプロトタイプベースのオブジェクト指向型言語である
JavaScriptはオブジェクト指向型言語の中では比較的マイナーなプロトタイプベースのオブジェクト指向だと言われている。
だが、中には、JavaScriptを以下のように評価している人もいる。
- JavaScriptはプロトタイプベースではない
- JavaScriptはクラスベースでもある
- JavaScriptはやっぱりプロトタイプベース
もはやわけわかめだが、実はこれには複雑な事情がある。今回はこれについて書いていく。
そもそもプロトタイプベースとは
プロトタイプベースとは、オブジェクト指向の一手法である。クラスベースの場合、クラスからインスタンスを生成することでオブジェクト指向を実現すると先ほど書いたが、プロトタイプベースの場合は、プロトタイプ(オブジェクト)を複製(クローン)することでオブジェクト指向を実現する。ここから賢明な人ならわかると思うが、プロトタイプベースにはクラスとインスタンスの区別がない。
このプロトタイプベースは、Smalltalkのクラスベースへのアンチテーゼとして派生されたオブジェクト指向である。Smalltalkのクラスベースのオブジェクト指向をより単純にしよう、と考え作られたのがプロトタイプベースである。
Ioを例にプロトタイプベースを体験してみる
「JavaScriptはプロトタイプベースではない」という人の主張には「SelfやIoとは違う」というのがあるようだ。特にIoと比べた記事が多かった。Selfを例に出しても良いがおそらくIoから本来のプロトタイプベースオブジェクト指向プログラミング(以下OOP)を知った方も多いと思われるので、こちらでもIoを例にプロトタイプベースOOPをやってみる。
Ioは純粋なプロトタイプベースのオブジェクト指向型言語である。Ioでプロトタイプのクローンとスロット*1の定義をしてみる。
// (1) 複製して新しいオブジェクトを作る CloneObject := Object clone // (2) スロットの定義 CloneObject hello := method("Hello, " .. self lang .. "!") CloneObject lang := "Io" // (3) helloメッセージをCloneObjectに送る CloneObject hello println
これを実行すると以下のようになる
Hello, Io!
これらが順番にどうなっていっているのか、図を用いながら解説していこうと思う。
(1) 複製して新しいオブジェクトを作る
IoではすべてのオブジェクトはObjectをクローンするところから始まる(なんかこういうところはSmalltalkぽいっすね*2。まぁSmalltalkから影響受けているから当然だと思うけど)。
(2) スロットの定義
スロットを定義する。今回はhello
メソッドとインスタンス変数lang
を定義した。hello
メソッドは"Hello, <インスタンス変数lang>!"
を返す。
(3) helloメッセージをCloneObjectに送る
Ioにはメッセージパッシングの概念があり、レシーバに対しメッセージを送ることでそれに対応したメソッドの結果が返ってくる。今回の場合はCloneObject
に対してhello
メッセージを送ることで"Hello, Io!"
が返ってきている。
ここでSmalltalkと違うのはnewを使わずにCloneObject
に直接new以外のメッセージを送っていることである。これが「クラスとインスタンスの区別がない」ということである。
ちなみに最後にprintln
メッセージが書かれている。これはhello
メッセージの結果("Hello, Io!"
)をレシーバにしてそれに対してメッセージを送っている。println
メソッドはオブジェクトを標準出力するメソッドである。
継承
なお、Ioでは継承もクローンすることで簡単に実現できる。今回はCloneObject
をクローンしている。
// 継承もクローンすることで実現する DeriveObject := CloneObject clone
プロトタイプチェーン
プロトタイプベースではクローン(継承)をすることで、クローンしたオブジェクトはクローン元のオブジェクトのスロットも使うことができる、が、クローンしたオブジェクトがそのスロットを持っているわけではない。ではどのようにしてクローン元にあるスロットを使うのかと言うと、プロトタイプチェーンと呼ばれるものを辿ってクローン元にあるスロットを使うようにしている。
たとえば、先ほどクローンしたDeriveObject
にhello
メッセージを送るとする。
// helloメッセージをDeriveObjectに送る DeriveObject hello println // DeriveObject -> CloneObject hello
するとDeriveObject
にはhello
メソッドはないので、プロトタイプチェーンを辿ってCloneObject
のhello
メソッドを使う。
そうすることで、問題なくhello
メッセージの応答が返ってくるわけである。
Hello, Io!
JavaScriptで行うプロトタイプベースOOP
JavaScriptのプロトタイプベースOOPは本当に奇妙である。OOPであることは間違いないと思うのだが、非常にわかりにくい。
そして驚くべきは、JavaScriptは第一級関数によって(非純粋ではあるものの)関数型プログラミングとオブジェクト指向プログラミング(一部)の両方を実現できることである。これが奇妙さに拍車をかけている。
オブジェクトの基本
JavaScriptでオブジェクトを生成するには、連想配列を定義する。これがオブジェクトの基本形である。
// 基本的なオブジェクトの生成 var object_1 = { name: "LiveScript", hello: function() { return "Hello, " + this.name + "!"; }, }; console.log(object_1.hello());
Hello, LiveScript!
関数オブジェクトの生成
JavaScriptでクローンを実現するためには関数を定義する必要がある。これはコンストラクタ関数と呼ばれるものである。
// コンストラクタ関数を定義 function Object_1(name) { this.name = name; }
ちなみにJavaScriptでは関数リテラルも代入できるので、以下のような方法もある。人によってはこっちのほうが見やすいかも。
// コンストラクタ関数を定義 var Object_1 = function(name) { this.name = name; }
prototypeプロパティにプロパティを定義
JavaScriptでは関数を定義する(もしくは関数リテラルを代入する)と、一つおまけがついてくる。それがprototype
プロパティであり、このprototype
プロパティが後々クローンに相当する機能のキモとなってくる。
ちなみにJavaScriptにおけるプロパティはプロトタイプベースにおけるスロットに相当し、PythonやC#、Kotlinなどのプロパティとは違うので混同しないよう注意する必要がある。
今回は先ほど生成したオブジェクトobject_1
を代入する。
// prototypeプロパティにプロパティを定義
Object_1.prototype = object_1;
インスタンス生成(クローンに相当)
では、ついにクローンを行う。JavaScriptにおけるクローンはnew
演算子によるインスタンス生成によって行う。
// インスタンス生成(≒オブジェクトのクローン) var object_2 = new Object_1("JavaScript"); console.log(object_2.hello());
Hello, JavaScript!
これにより、まずobject_1
をクローンし、コンストラクタ関数によりobject_2
にname
プロパティを追加している(書き換えるのではない、理由は後述)。
継承も同じようにして行う(実はすでに継承はしているが、念のため)。
// クローン(継承)するためのコンストラクタ関数を定義 // 継承後に新しく定義するプロパティもここでまとめて定義可能 var Object_2 = function(name) { this.derived_name = name; this.history = function() { return this.derived_name + " is derived from " + this.name + "."; } } // prototypeプロパティにプロパティを定義 Object_2.prototype = object_2; // インスタンス生成(≒オブジェクトのクローン) var object_3 = new Object_2("TypeScript"); console.log(object_3.history());
TypeScript is derived from JavaScript.
ちなみにIoと同じくインスタンス生成後もプロパティの定義をすることは可能である。
// クローンした後も新しいプロパティの定義はできる object_3.prop_new = "New property"; console.log(object_3.prop_new);
New property
プロトタイプチェーン
JavaScriptにもプロトタイプチェーンの概念がある。そのためクローンしたオブジェクトもクローン元にあるプロパティを問題なく呼び出すことが可能である。
// object_3 -> object_2 -> object_1.hello()
console.log(object_3.hello());
Hello, JavaScript!
わかりづらすぎて後に糖衣構文が追加された
さて、このJavaScriptのプロトタイプベース、見ていてどう思っただろうか。
おそらくクラスベースに慣れている人の大半はわかりづらいと思っただろう。Ioの純粋なプロトタイプベースよりもわかりづらいのではないだろうか。
「JavaScriptはプロトタイプベースではない」という意見はあるものの、理解してみればJavaScriptのプロトタイプベースは確かにプロトタイプベースである、と自分は思った。しかしオブジェクトのクローン(JavaScriptで言えばインスタンスの生成)に関数が必要など無理のある設計であったことは間違いないと思っている*3。
その影響か、ECMAScript5*4にオブジェクトのクローンに相当する機能を簡単に扱えるようにするObject.create()
メソッドが追加され、さらにはECMAScript2015(6に相当)にはクラス構文(class
)まで追加されてしまったのである。
ちょっと待って!じゃあJavaScriptはクラスベースなの?
と思う方もいるだろう。これは断じて間違いである。確かにこのECMAScript2015に追加されたクラス構文によって「JavaScriptはクラスベースである」と勘違いする人は続出しただろう。
だがしかし、クラス構文はJavaScriptでクラスベースOOPができるようにするのではなく、クラスベース"ライク"なOOPをするための構文、つまり、糖衣構文*5である。したがって、クローンしたオブジェクトになくてクローン元にあるプロパティはプロトタイプチェーンを辿って呼び出すし、クラスとインスタンスの区別が明確になったわけではないのでJavaScriptは依然としてプロトタイプベースのオブジェクト指向型言語なのである。
おまけ:Object.create()を使った例とクラスを使った例の紹介
今回紹介したのは昔書かれていたJavaScriptコードである。それよりもObject.create()
による例とクラス構文による例を知ったほうが役立つと思うのでそちらを紹介して今回の締めとする。
Object.create()による例
// クローン元となるオブジェクト var object_1 = { name: "JavaScript", hello: function() { return "Hello, " + this.name + "!"; }, }; console.log(object_1.hello()); // オブジェクトのクローン var object_2 = Object.create(object_1); // プロパティの追加 object_2.derived_name = "TypeScript"; object_2.history = function() { return this.derived_name + " is derived from " + this.name + "."; } console.log(object_2.history());
Hello, JavaScript! TypeScript is derived from JavaScript.
クラス構文による例
// クラスの定義 class Object_1 { constructor(name) { this.name = name; } hello() { return "Hello, " + this.name + "!"; } } // インスタンスの生成 let object_1 = new Object_1("JavaScript"); console.log(object_1.hello()); // 継承 class Object_2 extends Object_1 { constructor(name_1, name_2) { super(name_1); this.derived_name = name_2; } history() { return this.derived_name + " is derived from " + this.name + "."; } } // インスタンスの生成 let object_2 = new Object_2("JavaScript", "TypeScript"); console.log(object_2.history());
Hello, JavaScript! TypeScript is derived from JavaScript.