@ledsun blog

無味の味は佳境に入らざればすなわち知れず

JavaScriptのプロトタイプ継承

JavaScriptをプロトタイプベースのオブジェクト指向言語と説明することがあります。 プロトタイプベースとはJavaScriptのどのような特徴を指しているのでしょうか?

プロトタイプベースとは何か?

まず最初に「プロトタイプベース」という言葉の意味を整理します。

プロトタイプベースとは、オブジェクト指向プログラミング言語のある性質を示す用語です。 対になる言葉として「クラスベース」があります。

プロトタイプベースが意味する性質とは?

プロトタイプベースは英語で prototype-based です。 直訳すると「プロトタイプに基づく」です。また、クラスベースの直訳は「クラスに基づく」です。

主語を補うと「オブジェクトの特性が○○に基づく」という意味です。つまり、

  • プロトタイプベース:オブジェクトはプロトタイプオブジェクトの特性を引き継ぐ
  • クラスベース:オブジェクトはクラスで定義された特性を持つ

特性とはオブジェクトの持つ処理と値のことです。 JavaScriptではプロパティ、JavaC#Rubyではフィールドとメソッドです。

プロトタイプ継承

継承 (プログラミング) - Wikipediaでは「継承」を次のように定義しています。

あるオブジェクトが他のオブジェクトの特性を引き継ぐ場合、両者の間に「継承関係」があると言われる。

たまに「プロトタイプチェインを使った擬似的な継承」と説明することがありますが *1 *2 、プロトタイプベースの「特性の引き継ぎ」も「継承」です。

プロトタイプベース プログラミング言語の特徴

プロトタイプベースとクラスベースの継承を比べると

  • プロトタイプベース:オブジェクトはプロトタイプオブジェクトの特性を引き継ぐ
  • クラスベース:サブクラスはスーパークラス特性を引き継ぐ

クラスベースでは継承にクラスを利用し「インスタンス化」と「継承」を区別しますが、 プロトタイプベースでは継承する際にクラスは必要ありません。例えば、Selfというプロトタイプベース言語はSmalltalkからクラスの文法を取り除いたものだそうです。*3

プロトタイプベースプログラミング言語は「インスタンス化」と「継承」を区別しない

JavaScript のプロタイプ継承

JavaScriptでプロトタイプ継承をしてみましょう。

親オブジェクト

var objA = {
    val1: 1,
    func: function() {
        return 2;
    }
};
objA.val1 + objA.func();

結果は 1 + 2 で 3 です。

Object.createスタイル

ECMAScript 5 に採用された Object.create を使うとシンプルに書けます。

objAがobjBのプロトタイプになり、objBはobjAのメソッドプロパティすべてを利用できます。

var objB = Object.create(objA);
objB.val2 = 3;
objB.val1 + objB.func() + objB.val2;

結果は1 + 2 + 3 で 6 です。

クラスを使った継承に慣れている人はobjAを「primaryな委譲オブジェクト」とイメージした方が分かりやすいと思います。 *4

クラシックスタイル

ECMAScript 3 でも動くプロトタイプ継承です。とてもややこしいです。

var Dummy = function() {};
Dummy.prototype = objA;
var objB = new Dummy();
objB.val2 = 3;
objB.val + objB.func() + objB.val2;

どういうわけか次の手順が必要です。

  1. 関数オブジェクトを用意する
  2. 関数オブジェクトの prototype プロパティに親オブジェクトに指定する
  3. new 演算子を使って関数オブジェクトを実行する

「これはひどい」と言わざるを得ません*5。このようなクソコードを書くと、プロトタイプベースの「インスタンス化と継承を区別しない」特徴が理解できなくなります。Object.create が使えるときは、必ず Object.create を使いましょう。

プロトタイプ継承の関数を自作する

Object.create が使えない環境では自作しましょう。

Object.create

JavaScript: The Good Parts の「3.5 プロトタイプ」に書いてあるのでパクリます。

Object.create = function(parent) {
    var F = function() {};
    F .prototype = parent;
    return new F();
};

Object.prototype.clone

Object.create は引数に親オブジェクトを取るので、Objectのインスタンスメソッドにするとよりオブジェクト指向っぽくなります。

Object.prototype.clone = function() {
    var Dummy = function() {};
    Dummy.prototype = this;
    return new Dummy();
};

こんな風に使う

var objB = objA.clone();
objB.val2 = 3;
objB.val1 + objB.func() + objB.val2;

Ioぽくってかっこいい!*6

プロトタイプベースの利点

プロトタイプベースではオブジェクトとクラスの関係がシンプルになります。

クラスベースの場合

メモリ上に展開されているクラスは、厳密にはクラスクラスのインスタンスオブジェクトでオブジェクトです。インスタンスオブジェクトと区別する場合はクラスオブジェクトと呼びます。

Java

クラスオブジェクトを利用すると、以下のような柔軟なプログラムを書くことができます。

  • リフレクション:クラスオブジェクトからクラス定義、メソッド定義を取得
  • AOP:クラスオブジェクトに動的にメソッド、プロパティを追加する

Ruby

Ruby ではもっとカジュアルにクラスオブジェクトを扱います。 オープンクラスを使ってメソッドを追加するのは、クラスオブジェクトの操作です。 さらにクラスメソッドを定義すると無名クラスが、特異メソッドを定義すると、特異クラスが現れます。 クラスオブジェクトを理解すると驚くほど柔軟なプログラミングができます*7

解説

しかし、クラスオブジェクトとインスタンスオブジェクトの関係を把握するのは困難です。 解説するのはもっと困難なので いまさらながらだけど、オブジェクトとクラスの関係を究めてみようよ - 檜山正幸のキマイラ飼育記 を読んでください。

プロトタイプベースの場合

プロトタイプベースではシンプルです。 オブジェクト間の関係は「継承」しかありません。 インスタンス化と継承を区別しません。

まとめ

プロトタイプベースプログラミング言語は「インスタンス化」と「継承」を区別しません。 プロトタイプベースの特徴を生かすために Object.create を使えるときは、必ず Object.create を使いましょう。

参考

図書

JavaScript: The Good Parts の 「3.5 プロトタイプ」にObject.createメソッドの解説が載っています。 プログラミング経験者向けのJavaScirpt文法解説書です。「付録A ひどいパーツ」「付録B 悪いパーツ」は必読です。

Ruby公式資格教科書 Ruby技術者認定試験 Silver/Gold対応 (EXPERT EXPASS)の「第4章 オブジェクト指向」にRubyのクラスオブジェクトのメモリイメージが詳しく解説されています。 それ以外にもRubyの文法解説書として文法のカバー範囲、説明の丁寧さがとても素晴らしいです。 プログラミング経験者がRubyの文法を知る際に役に立ちます。

メタプログラミングRubyの「第4章 木曜日:クラス定義」にRubyのクラスオブジェクトのメモリイメージが詳しく解説されています。 また、クラスオブジェクトに限らずRubyの活用例が多岐に渡って載っており、Rubyプログラマとしての発想力を広げてくれます。

ブログ

*1:http://builder.japan.zdnet.com/script/sp_javascript-kickstart-2007/20369885/

*2:http://d.hatena.ne.jp/yinkyweb/20110706/1309955433

*3:Rubyist Magazine - Rubyist のための他言語探訪 【第 3 回】 Io

*4:厳密には「生成時に指定された以降変更されない」点で委譲ではなく継承と区別できるようです。

*5:なぜ prototype というプロパティ名にしたのか?せめて prePrototype とかちょっと控えめな名前にしてくれれば。なぜnew 演算子でしか使わないのに事前に設定することにしたのか?new 演算子オペランドにしてくれれば。

*6:EcmaScript5 では Object.create をインスタンスメソッドにしていない。議論自体は知らないがDouglas Crockford の JavaScriptのプロトタイプ継承の追記を見るに、後方互換性を重視したのではないだろうか?

*7:興味がある人は「メタプログラミングRuby」を読んで下さい