なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

奇妙なプロトタイプベースオブジェクト指向型言語JavaScript

前の記事オブジェクト指向にはざっくり2つの考え方があると書いた。それとは別にオブジェクト指向を実現するためのアプローチとしてプロトタイプベースのオブジェクト指向がある。

オブジェクト指向には2つのアプローチがある

まずオブジェクト指向にはざっくり2つのアプローチがある。

そのうちの一つがクラスベースである。こちらは言わずもがな、クラスをもとにインスタンスを生成することでオブジェクト指向を実現する手法である。実はSmalltalkC++も、考え方は違うものの、同じクラスベースである

では、プロトタイプベースオブジェクト指向はご存じだっただろうか?このプロトタイプベース、実はとても有名なプログラミング言語に採用しているとされている。

それは、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) 複製して新しいオブジェクトを作る
f:id:opaupafz2:20210617184048p:plain
オブジェクトのクローン 

IoではすべてのオブジェクトはObjectをクローンするところから始まる(なんかこういうところはSmalltalkぽいっすね*2。まぁSmalltalkから影響受けているから当然だと思うけど)。

(2) スロットの定義
f:id:opaupafz2:20210617184925p:plain
スロットの定義

スロットを定義する。今回はhelloメソッドとインスタンス変数langを定義した。helloメソッドは"Hello, <インスタンス変数lang>!"を返す。

(3) helloメッセージをCloneObjectに送る
f:id:opaupafz2:20210617211552p:plain
メッセージの送信

Ioにはメッセージパッシングの概念があり、レシーバに対しメッセージを送ることでそれに対応したメソッドの結果が返ってくる。今回の場合はCloneObjectに対してhelloメッセージを送ることで"Hello, Io!"が返ってきている。
ここでSmalltalkと違うのはnewを使わずにCloneObjectに直接new以外のメッセージを送っていることである。これが「クラスとインスタンスの区別がない」ということである

ちなみに最後にprintlnメッセージが書かれている。これはhelloメッセージの結果("Hello, Io!")をレシーバにしてそれに対してメッセージを送っている。printlnメソッドはオブジェクトを標準出力するメソッドである。

継承

なお、Ioでは継承もクローンすることで簡単に実現できる。今回はCloneObjectをクローンしている。

// 継承もクローンすることで実現する
DeriveObject := CloneObject clone
プロトタイプチェーン

プロトタイプベースではクローン(継承)をすることで、クローンしたオブジェクトはクローン元のオブジェクトのスロットも使うことができる、が、クローンしたオブジェクトがそのスロットを持っているわけではない。ではどのようにしてクローン元にあるスロットを使うのかと言うと、プロトタイプチェーンと呼ばれるものを辿ってクローン元にあるスロットを使うようにしている
たとえば、先ほどクローンしたDeriveObjecthelloメッセージを送るとする。

// helloメッセージをDeriveObjectに送る
DeriveObject hello println  // DeriveObject -> CloneObject hello

するとDeriveObjectにはhelloメソッドはないので、プロトタイプチェーンを辿ってCloneObjecthelloメソッドを使う。

f:id:opaupafz2:20210619214804p:plain
プロトタイプチェーン

そうすることで、問題なく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におけるプロパティはプロトタイプベースにおけるスロットに相当し、PythonC#、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_2nameプロパティを追加している(書き換えるのではない、理由は後述)。

継承も同じようにして行う(実はすでに継承はしているが、念のため)。

// クローン(継承)するためのコンストラクタ関数を定義
// 継承後に新しく定義するプロパティもここでまとめて定義可能
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.

*1:プロトタイプベースで言うメソッドやインスタンス変数

*2:SmalltalkもObjectを継承するところからクラス定義を行う

*3:たとえばクローンにあたる機能を高階関数にしてコンストラクタ関数を渡すようにすれば良かったのではないかとかいろいろ考えてしまう。まぁ、今さら言っても遅いと思うが

*4:ECMAScriptJavaScriptにおける規格

*5:複雑もしくはわかりづらいコードを簡単に記述するための構文