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

前置き

「初めてのRuby(Yugui, オライリー)」の中に、オブジェクト指向にはクラスベースとプロトタイプベースの2種類がある、とあります。これを読んだとき、プロトタイプベースって何? と思ったのですが、調べてみると、JavaScriptがその代表ということでした。

その後「JavaScript: The Good Parts(Douglas Crockford, オライリー)」を読み、使いこなす自信は無いものの、プロトタイプベースのオブジェクト指向については理解できたと思いますので、ここでまとめておきます。

本文

表記

obj1がオブジェクトの場合、本稿ではこれを、オブジェクト名と点線と矩形を使って以下のように表記します。

オブジェクト

オブジェクトobj1が別のオブジェクトobjBaseをプロトタイプとする場合、本稿ではこれを、オブジェクト間の矢印を使って以下のように表記します。

プロトタイプ

「オブジェクトAが別のオブジェクトBをプロトタイプとする」と言った場合、オブジェクトAがオブジェクトBの属性とメソッドを継承していることを意味します。これが「プロトタイプベース」と呼ばれる所以でしょうか。上図の矢印は、継承元オブジェクトを指す矢印ということになります。

ObjectとFunction

JavaScriptでは、関数もオブジェクトの一種です。ObjectとFunctionは組み込みの関数オブジェクトで、Function.prototypeをプロトタイプとします。さらにFunction.prototypeは、Object.prototypeをプロトタイプとします。

ObjectとFunction

オブジェクトリテラル

リテラルでオブジェクトを定義すると、そのオブジェクトは、Object.prototypeをプロトタイプとします。

obj

関数オブジェクト

関数オブジェクトを定義すると、その関数オブジェクトは、Function.prototypeをプロトタイプとします。さらにFunction.prototypeがObject.prototypeをプロトタイプとするのは前述の通りです。

func

関数からオブジェクトを生成する

関数オブジェクトには、一般に言われる関数的にコールする以外に、もう1つの使い方があります。それは、new演算子のオペランドに使うというものです。また、このときの関数オブジェクトには、newで生成されるオブジェクトのプロトタイプを決めるという役割もあります。

これまでの説明で登場したオブジェクトのプロトタイプは、Object.prototypeやFunction.prototypeのように、あらかじめ決められたものでした。では、オブジェクトのプロトタイプを任意に制御するにはどうすれば良いのでしょうか。ここで、関数オブジェクトとnewを使うわけです。

すなはち…

newに関数を指定して新しいオブジェクトを生成すると、そのオブジェクトは、関数のprototype属性にセットされていたオブジェクトをプロトタイプとします。

関数(非継承)

関数を定義したとき、デフォルトでは、その関数オブジェクトのprototype属性には適当な空オブジェクトのようなもの(実際は空ではなくconstructor属性を持ったオブジェクトですが、このconstructor属性は大して機能してないので実質的に空と考えても良いです)がセットされており、この空オブジェクトはObject.prototypeをプロトタイプとします。

newする前に、関数オブジェクトのprototype属性に任意のオブジェクトをセットしておけば、そのオブジェクトをプロトタイプとする新しいオブジェクトを生成することができます。

擬似クラス

前項のnewに使った関数は、クラスベースのオブジェクト指向で言うところのコンストラクタのような働きをしています。上図で、rect1→Rect.prototype→Object.prototypeとプロトタイプのリンクがつながっているところは、rect1がRectという擬似クラスのインスタンスで、RectクラスがObjectクラスの派生クラスになっていると考えることもできそうです(ただし、rect1が引き継ぐのはRect.prototypeの属性とメソッドであり、Rectの属性とメソッドではないという点に注意して下さい ←これ、ホントかなぁ…)。

また、デフォルトでコンストラクタ関数のprototype属性に空オブジェクト(のようなもの)がセットされているところは、特に指定しなければ全てのクラスはObjectクラスから派生するという、お馴染みのモデルを連想させます。

更にクラス的な振る舞いを真似るなら、コンストラクタのprototype属性に別のコンストラクタからnewしたオブジェクトをセットしておくと、擬似クラスを別の擬似クラスから派生させることができます。

関数(継承)

ShapeからnewしたオブジェクトをRect.prototypeにセットしてあるので、プロトタイプのリンクは、rect1→Rect.prototype→Shape.prototype→Object.prototypeとなります。

……? 待てよ

ちょっと脱線して、JavaScriptでもクラスベースのオブジェクト指向ができるということを力説してしまいました。しかし、本稿の主題はプロトタイプベースのオブジェクト指向でしたね。プロトタイプベースでも擬似クラスでも、オブジェクトの生成にはnewと関数を使うので混乱してしまいがちです。プロトタイプベースとクラスベースとの違いは以下の点でしょう。

  • プロトタイプベースでは、newするときの関数の内容は重視せず(極端に言えば空関数で良い)、関数のprototype属性にセットされたオブジェクト(および、そのオブジェクトの属性やメソッド)を重視する
  • Shapeを例にすると、擬似クラスでは、継承させたい属性やメソッドをShape.prototypeに付け足していくが、プロトタイプベースでは、継承させたい属性やメソッドを持ったオブジェクトをShape.prototypeにセットする(結果的に同じだが、アプローチが違う)

「JavaScript: The Good Parts」の著者Douglas Crockfordいわく、擬似クラスに関するJavaScriptの仕様はBad partsです。そのため、newやコンストラクタを(直接には)使わずにオブジェクトを生成することを薦めています。

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

このように、コンストラクタは空っぽにして、newする部分を隠蔽したcreateメソッドをObjectに定義し、継承元としたいオブジェクトを引数に指定して新しいオブジェクトを生成させます。ここ以外の場所ではnewを一切使いません。これによりコンストラクタ的な関数も使わなくなるので、JavaScriptから擬似クラスの側面を排除することができます。

サンプルコード

ここまでの説明とほぼ同じことをコードで再現すると、下記のような感じです。

// トップレベルの変数は、グローバルオブジェクトの属性に相当する。
var var1 = 3;

function tutor()
{
  // ObjectもFunctionも関数。
  printLog(typeof Object);        // function
  printLog(typeof Function);      // function

  // Objectは、Object.prototypeをプロトタイプとする。
  printLog(Object.aaa);           // undefined
  Object.prototype.aaa = "aaa";
  printLog(Object.aaa);           // aaa

  // Functionは、Function.prototypeをプロトタイプとする。
  printLog(Function.bbb);         // undefined
  Function.prototype.bbb = "bbb";
  printLog(Function.bbb);         // bbb

  // Function.prototypeは、Object.prototypeをプロトタイプとする。
  printLog(Function.aaa);         // aaa

  // オブジェクトリテラルで定義したオブジェクトは、
  // Object.prototypeをプロトタイプとする。
  var obj1 = {};
  var obj2 = {
    prop1: "val1",
    prop2: "val2"
  };
  printLog(obj1.aaa);             // aaa
  printLog(obj2.aaa);             // aaa

  // ただし関数オブジェクトの場合は、
  // Function.prototypeをプロトタイプとする。
  var func1 = function() {
    printLog(this.var1);
  }
  printLog(func1.aaa);            // aaa
  printLog(func1.bbb);            // bbb

  // 定義したオブジェクトが非関数の場合、
  // そのオブジェクト自体のprototype属性は未定義である。
  printLog(obj1.prototype);       // undefined

  // 関数オブジェクトを定義すると、
  // そのprototype属性にはオブジェクトがセットされ
  // そのオブジェクトのconstructor属性には関数自身がセットされる。
  var Shape = function() {
    this.left = 0;
    this.top = 0;
  };
  printLog(typeof Shape.prototype);   // object
  printLog(typeof func1.prototype);   // object
  if ( Shape.prototype.constructor !== Shape ) abort();
  if ( func1.prototype.constructor !== func1 ) abort();
                        // 実のところ、constructor属性を意識する機会は無いようだ。

  // newにより生成したオブジェクトは、
  // 関数のprototype属性にセットされたオブジェクトをプロトタイプとする。
  var shape1 = new Shape();
  var shape2 = new Shape();
  if ( shape1.constructor !== Shape ) abort();
                        // Shapeは、クラスのコンストラクタ的に働く。
                        // Shape自体はObject.prototypeをプロトタイプとする。
                        // リテラルで定義したので。
  Shape.prototype.printPosition =
    function() { printLog("(" + this.left + ", " + this.top + ")"); }
                        // Shape.prototype.printPositionに関数を定義することは
                        // Shape.printPositionに関数を定義することとは意味が異なる。
                        // 前者がインスタンスメソッドで、後者がクラスメソッドのような感じ。
  shape1.printPosition();         // (0, 0)

  // 関数のprototype属性に、別関数からnewしたオブジェクトをセットすると
  // クラスの継承に似た挙動を実現できる。
  var Rect = function() {
    this.width = 1920;
    this.height = 1080;
  };
  Rect.prototype = new Shape();
  Rect.prototype.right = function() { return this.left + this.width; };
                        // Rect.prototypeの属性を変更することは、
                        // Shapeの1インスタンスの属性を変更することに過ぎない。
                        // Shape自体やShape.prototypeには影響がない。
  var rect1 = new Rect();
  var rect2 = new Rect();
  printLog(rect1.left);           // 0
  printLog(rect2.right());        // 1920
  rect1.printPosition();          // (0, 0)
                        // rect1はRect.prototypeをプロトタイプとする。
                        // Rect.prototypeは、Shape.prototypeをプロトタイプとする。
                        // つまりrect1は、RectとShapeの属性を合わせ持つ。

  // Douglas Crockfordいわく、newや擬似クラスは悪である。
  // これは、クラスベースのオブジェクト指向への中途半端な歩み寄りであり、
  // 混乱を招くし、プロトタイプベースのオブジェクト指向へのパラダイムシフトを妨げる。
  // また、(newなしで)関数的に使われる関数と、(newを伴って)コンストラクタ的に
  // 使われる関数とを区別する手段が無いので、誤用によるバグが生じやすい。
  // そこで彼は、newを一切使わないことを薦めている(ただし一ヶ所を除いて)。
  // その一ヶ所がObject.create()だ。
  Object.create = function (o) {
    var F = function() {};
    F.prototype = o;
    return new F();
  }
  var rect3 = Object.create(shape1);
  rect3.width = 1920;
  rect3.height = 1080;
  rect3.right = function() { return this.left + this.width; };
  var rect4 = Object.create(rect3);
  printLog(rect4.right());        // 1920

  // ■余談~thisが指すもの
  // コンテキスト次第で、thisが指すものが異なる。
  // CASE1: オブジェクトのメソッドの中では、オブジェクトを指す。
  //        rect1.right()内では、this === rect1
  // CASE2: 関数の中では、グローバルオブジェクトを指す。
  func1();                        // 3
                        // この仕様は直感的ではない。
                        // 関数の呼び出し元のthisと同じものを指すべきだろう。
                        // このケースでは、関数呼び出し前に、thatにthisを代入しておき、
                        // 関数内ではthatを使うのがgood practice。
  // CASE3: newしたときの関数内では、生成したオブジェクトを指す。
  //        var rect1 = new Rect()内では、this === rect1
  // CASE4: apply()の中では、apply()の第1引数を指す。
  //        rect1.right.apply(rect2)内では、this === rect2
}
Last modified:2011/01/14 13:41:16
Keyword(s):
References:[言語Tips]
This page is frozen.