newを封印して、JavaScriptでオブジェクト指向する(2)

Posted by Hiraku on 2011-05-08

前回の続きです。サンプルコードは前回から続いていると思ってください。

  1. privateは諦めましょう
  2. 親のメソッドを呼ぶ
  3. コンストラクタ
  4. instanceofに対応する
  5. ダックタイピングのススメ

今回もobject関数を使うので再掲載。

/*
 * object - オブジェクトを作る
 * Object object(BaseObj [, mixinObj1 [, mixinObj2...]])
 */
function object(o) {
  var f = object.f, i, len, n, prop;
  f.prototype = o;
  n = new f;
  for (i=1, len=arguments.length; i<len; ++i)
    for (prop in arguments[i])
      n[prop] = arguments[i][prop];
  return n;
}
object.f = function(){};

privateは諦めましょう

メンバー変数のスコープは、他の言語ならprivate, public, protectedなど用意されていますが、JavaScriptでは全てのメンバが常にpublicです。そしてprivateを無理矢理実現することは、できなくはないですが、やめた方がいいです。

「これがJavaScriptにおける完璧なカプセル化だ!」みたいな記事はたくさんありますが、全然カプセル化できていなかったり、すごく不便だったり(privileged系)、メモリ消費が激しく継承を併用出来なかったり(クロージャでごまかした系)、必要なライブラリが膨大だったり(独自でオブジェクトシステム作っちゃった系)して、どれも完全にはうまくいっていないはずです。

私もやろうと思った時期はありますが、全部publicで作るのが言語的に自然だと思うようになりました。言語的なサポートは諦めて、コーディング規約でがんばりましょう。

親のメソッドを呼ぶ

継承はしたいけれど、ほんの少しだけ処理を加えるだけでいいのに…ということもあるでしょう。JavaScriptではapplyやcallを使うことで、親のメソッドをいつでも呼ぶことができます。とりあえずは{親オブジェクト名}.{メソッド名}.apply(this, arguments)と書くのが一番確実です。

superやparentといったキーワードは使わず、親の名前を直接使います。JavaScriptでは多重継承もどきが可能なので、親が一意に決まらないからです。

var Penguin = object(Bird, {
  name: "ペンギン"
, fly: function(){
    Bird.fly.apply(this, arguments); //Birdのflyメソッドを実行する
    alert("ぺんぎんは飛べない…");
  }
});

Penguin.fly(); // ぱたぱた ぺんぎんは飛べない…

コンストラクタ

object関数はオブジェクトを作るところまでしか行わないので、何か初期処理が必要な場合は使う側で明示的に呼ぶ必要があります。個人的にはinitというメソッドを作って呼ぶようにしていますが、別に名前は何でもいいです。

コンストラクタは最後にreturn thisしておくと、オブジェクトを作ってすぐ呼べるようになります。

var Timer = {
  start: null

, init: function(){
    this.start = +new Date;
    return this;
  }

, toString: function(){
    return (+new Date - this.start) + "ms経過";
  }
};

//コンストラクタを起動する
var timer = object(Timer);
timer.init();
//return thisしてあればこう書いてもよい
var timer = object(Timer).init();

setTimeout(function(){ alert(timer) }, 1000); //1秒後に“1000ms経過”が表示

ちなみにJavaScriptでデストラクタを実現することはできません。オブジェクトの解放タイミングは処理系任せだからです。何か終了処理が必要な場合は、専用のメソッドを作っておいて明示的に呼ぶ必要があります。

instanceofに対応する

ここまで述べてきたオブジェクト指向のやり方はnewを使う標準的な方法とは違うため、いくつか問題が発生します。その一つがinstanceof演算子です。

instanceof演算子はA instanceof Bの形で使い、AオブジェクトがB"コンストラクタ"を継承しているときだけtrueを返します。Bにオブジェクトではなく"コンストラクタ"を入れなければならないため、うまく動きません。

どうしても使いたい場合は、以下のような補助関数を作っておけばいいです。

function type(o) {
  function f(){}
  f.prototype = o;
  return f;
}

alert(Bird instanceof type(Animal)); //true

ただし、ユーザー定義のオブジェクトに対してはinstanceofは使うべきではないです。硬めな言語を使ってきた人にはにわかに信じられないかも知れませんが、instanceofがtrueだからといって本当に継承されているとは限りません。いくらでも書き換え可能だからです。

// うどんクラス
var Udon = {
  koshi: 9
, nodogoshi: 5
};

var udon = object(Udon);
udon.koshi = null; //ゆですぎた

alert(udon instanceof type(Udon)); // true

Udonクラスからうどんを作りましたが、ゆですぎてコシが無くなってしまいました。こいつの出自は確かにうどんです。しかし本当にうどんと呼べるのでしょうか? 少なくともプログラムの世界ではうどんとして役立たずです。

JavaScriptの動的な世界では出自に大した意味はないのです。学歴?家柄?それが何の保証をしてくれるというのでしょう。大事なのは「そのオブジェクトにどんな能力があるか」ではないでしょうか。この、ある意味で現実的な考え方に基づく型の判別方法は、ダックタイピングと呼ばれます。

ダックタイピングのススメ

動的な言語で型判別をするときに使うべきなのはinstanceofではありません。ダックタイピングです。これはオブジェクトが必要な要素を全て持っているかどうかで判別するやり方です。厳密さのレベルによって実装の仕方は異なりますが、例えば次のような関数で実現することができます。

// ダックタイピングによる型判別関数
// プロパティの型だけ比較するタイプ
function is(obj, proto) {
  for (var p in proto) {
    if (typeof proto[p] != typeof obj[p])
      return false;
  }
  return true;
}

// かまぼこクラス
var Kamaboko = {
  syokkan: "ぷりぷり"
};

// かにクラス
var Kani = {
  syokkan: "ぷりぷり"
, fuumi: "かに"
};

// カニカマを作る
var kanikama = object(Kamaboko, {
  fuumi: "かに"
});

alert(kanikama instanceof type(Kani)); //false
alert(is(kanikama, Kani)); //true

カニカマはカニではありません。しかし必要な要素を満たしていれば、出自がカマボコだろうがカニカマはカニなのです。

少なくとも出自が違うからと拒絶するより、よっぽど現実的で素敵な世界だと、私は思います。

まとめ

これで言いたかったことはだいたい全部吐き出せました。インスタンス化した後でクラス側をじゃんじゃん書き換えられる、なんていうのは動的な言語の大きな特徴ですね。まだまだ面白い使い方があると思います。

instanceofをさんざんdisってしまいましたが、組み込みのオブジェクトの判定とか、使う場面はある演算子です。使わない方がいいのはユーザー定義のオブジェクトのみ、ってことにしておいてください。

オブジェクト指向はそこそこの規模にならないと使わないですが、うまく使えば1万行が1000行に収まったりすることもざらにあるので、テクニックとして覚えておくといいです。

keyword: javascript

JavaScriptの最新記事