みなさん、こんにちは。OPTiM Storeの開発チームの半田です。 業務内容とはあまり関係ありませんが、学習初期に疑問に思っていたことが今になってやっと理解出来たので、今回文章に纏めてみようと思います。
ことはじめ
JavaScriptにおいて、『オブジェクト』という言葉は二重に使用されています。
まず一つ目は、オブジェクト指向ブログラミングの文脈におけるオブジェクト、すなわち属性と手続きをひとかたまりにした実体(インスタンス)のことです。
二つ目は、連想配列です。
前者はどのオブジェクト指向プログラミング言語でも共通して『オブジェクト』と呼ばれていますが、後者を『オブジェクト』と呼ぶのはJavaScript独特のものです。
以下混乱を避けるため、前者を「インスタンス」、後者を「連想配列」と呼びます。
明確な定義が必須であるプログラミングの世界において言葉の二重使用は珍しく、学習開始当初から違和感がありました。
今回本件を調べてみて、この二重使用には言語構造上の明確な理由があることがわかりました。
ポイント
- JavaScriptのオブジェクト指向はクラスベースでなくプロトタイプベースである。
- JavaScriptは関数を第一級オブジェクトとして扱うプログラミング言語である。
- JavaScriptにおいて、インスタンスは連想配列である。
環境
Node.js v14.4.0を使用しています。
オブジェクト指向の種類
通常オブジェクト指向と言った場合それはクラスベースのオブジェクト指向を差すと思います。JavaもC++もRubyもPythonもクラスベースのオブジェクト指向です。
一方、JavaScriptはプロトタイプベースのオブジェクト指向です。
前者は、抽象概念として定義されたクラスを基に具体概念としてインスタンスを生成する流れを持ちます。
後者には抽象概念としてのクラスは存在せず、具体的な別のインスタンスのコピーとして新たなインスタンスを生成します。
コード1-1はJavaでインスタンスが生成される様子を描いたものです。ManクラスをProgrammerクラスが継承し、Programmerクラスに"太郎"という引数を渡してインスタンス化しています。
●コード1-1
class Man { String name; Man(String name){ this.name = name; } void greeting(){ System.out.println("こんにちは、"+this.name+"です。"); } } class Programmer extends Man { Programmer(String name){ super(name); } void work(){ System.out. println("開発大好き!"); } } public class Main { public static void main(String[] args) throws Exception { Programmer taro = new Programmer("太郎"); taro.greeting(); // => こんにちは、太郎です。 taro.work(); // => 開発大好き! } }
他のクラスベースの言語でも似たような文法を使います。
しかし、JavaScriptにはclassを定義する文法が存在しません。※ES2015からは実は存在しますが、単なる糖衣構文であるためないものとします。
プロトタイプベースのオブジェクト指向でのインスタンス生成
クラスの概念のないJavaScriptにおいては、まず関数(コンストラクタ)を裸で定義して、new演算子を用いてインスタンス化します。(コード2-1)
コンストラクタは戻り値のない関数で、Javaで言うフィールドをセットする役目を主に果たします。
●コード2-1
function Constructor(){ this.key = "value"; } const instance = new Constructor();
実はこのインスタンスを出力すると、関数(コンストラクタ)内で定義した値を持つ連想配列になっています。(コード2-2)
●コード2-2
console.log(instance); // => Constructor { key: 'value' }
あとは、参照させたい関数(メソッド)をprototypeプロパティにセットしてゆきます。(コード2-3)
●コード2-3
Constructor.prototype.func = function(){ console.log("プロトタイプに登録された関数です") }
ここでもう一度instanceを出力しても加えたfuncは連想配列に入っていませんが、instanceに紐づけられているプロトタイプを辿ると見ることができます。(コード2-4)
プロトタイプはObject.getPrototypeOf関数で見ることができます。
●コード2-4
console.log(instance); // => Constructor { key: 'value' } console.log(Object.getPrototypeOf(instance)); // => { func: [Function (anonymous)] }
インスタンスを別のコンストラクタ内部で呼び出しプロトタイプを登録することで、継承を実現できます。(コード2-5)
もしこの作業を連続させてゆけば、インスタンスのプロトタイプ、その継承先のプロトタイプ、そのまた継承先のプロトタイプ...とプロトタイプを連ねることができます。これをプロトタイプチェーンと言います。
●コード2-5
function ChildConstructor(){ Constructor.call(); } ChildConstructor.prototype = Object.create(Constructor.prototype)
コード1-1と同じようなことをJavaScriptで行おうと思うと以下のようになります。(コード2-6)
●コード2-6
function Man(name){ this.name = name; } Man.prototype = { greeting: function(){ console.log(`こんにちは、${this.name}です。`); } }; function Programmer(name){ Man.call(this, name); } Programmer.prototype = Object.create(Man.prototype); Programmer.prototype.work = function(){ console.log("開発大好き!"); }; const taro = new Programmer("太郎"); taro.greeting(); // => こんにちは、太郎です。 taro.work(); // => 開発大好き!
このプロトタイプチェーンを出力すると以下のようになります。(コード2-7)
最終的に空の連想配列に行きつき、その先はnullになります。
●コード2-7
const __getProto__ = Object.getPrototypeOf; // 名前が長くて見にくいので定義し直します console.log(taro) // => { name: '太郎' } console.log(__getProto__(taro)) // => { work: [Function (anonymous)] } console.log(__getProto__(__getProto__(taro))) // => { greeting: [Function: greeting] } console.log(__getProto__(__getProto__(__getProto__(taro)))) // => {} console.log(__getProto__(__getProto__(__getProto__(__getProto__(taro))))) // => null
関数の扱い
コード2-6で当たり前のように関数を変数に代入したり連想配列の値としている箇所がありますが、多くの言語ではこれができません。
例えば、Javaでは以下のような文法がありません。(コード3-1)
●コード3-1
public class Main { public static void main(String[] args) throws Exception { Function func = void (){ System.out. println("関数は変数に格納できないみたいです"); } } } // => error: illegal start of expression
Rubyにもありません。(コード3-2) ※ただしProcを使えば似たようなことができます。
●コード3-2
func = def puts "関数は変数に格納できないみたいです" end # => syntax error, unexpected string literal, expecting ';' or '\n'
JavaScriptでは変数に関数を代入できます。(コード3-3)
●コード3-3
const func = function(){ console.log("関数を変数に格納できました") }
これが可能なのは、JavaScriptが関数を第一級オブジェクトとする設計になっているからです。この性質により、変数に代入したり、他の関数の引数や戻り値として使用したり、配列や連想配列の値とすることができます。
この性質こそが、関数を他の文字列や数値データと同じように連想配列に格納することを可能にし、「連想配列がインスタンスそのものである」という事実を支えています。
『オブジェクト』という言葉が二重で使われているのは、二重も何も同じものだったからでした。
ところでなんでプロトタイプベースのオブジェクト指向はこんなにもマイナーなのだろう?
プログラミング言語はコンピュータに対する命令文です。コンピュータが読むものなので、コンピュータが理解できるものでなければなりません。
ですが人間とコンピュータでは考え方が違います。人間の立場から見ればコンピュータは、異様に愚直で、柔軟性に欠け、気が利きません。
開発の規模が小さかった昔はコンピュータ寄りの考え方を必死にして記述をしていたようですが、時代を経るにつれて開発の規模も大きくなりそれに伴い指数関数的に複雑さを増してゆき人間の脳が追いつかなくなり、言語そのものへの工夫が必要となったようです。
人間にもわかりやすいのにコンピュータもきちんと理解ができるオブジェクト指向はその工夫の最たるもので、もはや芸術と言えると個人的には思います。
このオブジェクト指向の中でクラスベースがメジャーとなったのは、全てのモノは何らかの抽象カテゴリに属していてそれらが層をなしているという構造が、単に人間の直感に促していたからだと思われます。
しかし、現実のモノは抽象概念から生み出されるわけではありません。我々は親というプロトタイプを継承しているし、自然のあらゆるものがそういった構造をしています。コンピュータにとってもプロトタイプベースのJavaScriptはとても軽量で読みやすいようです。
そう考えると、現実を直視したものでない抽象概念という独特の理解の仕方をする人間ってなんなのかな....と話がそれてきたので今回はこの辺で締めようと思います。
オプティムでは、一緒に働く仲間を募集しています。興味のある方は、こちらをご覧ください。