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

Posted by Hiraku on 2011-05-07

以下はプロトタイプ的継承だけで一通りの機能を実現できる、という一つの例です。もちろん他にも書き方はありますし、newを使うのがよくないと主張しているわけではないです。
(誤解を生みそうな文体が混じっているようなので追記: 2011/5/14)

春ですし、少し初心者向けの記事を書きます。タイトル通り、JavaScriptのオブジェクト指向について。ちょっと長くなるので目次です。

  1. クラス(に相当するオブジェクト)を作る
  2. オブジェクトからオブジェクトを作る(インスタンス化)
  3. 単一継承
  4. 多重継承
  5. privateは諦めましょう
  6. 親のメソッドを呼ぶ
  7. コンストラクタ
  8. instanceofに対応する
  9. ダックタイピングのススメ

JavaScript標準のオブジェクト指向といえばnewやらprototypeやらを書く必要がありますが、これらは書くのが面倒臭い上に気をつけないといけない点がたくさんあります。JavaScript: The Good PartsでもBad Partsに選別されているほどです。

ではどう書けばいいか。こんな補助関数を一つ定義してください。そしてnewを封印しましょう。これさえあればJavaScriptのオブジェクト指向なんて簡単です。

/*
 * 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(){};

もしこの関数が中で何をやっているかわからなくても、そんなに困らないと思います。以下は使い方だけ説明していきます。

クラス(に相当するオブジェクト)を作る

クラスと書きましたが、JavaScriptにクラスはありません。この言葉は何度言っても言いすぎではないほどに重要です。もしJavaScriptの本で補足も何もなく「クラスを作ろう!」みたいなことが書いてあったら、それはJavaScriptを理解していない人が書いた本です。とっととゴミ箱へダンクしましょう。

JavaScriptにはオブジェクトと呼ばれる機構しか存在しないです。そして、どのオブジェクトも「クラス」のように扱うことができます。なぜならオブジェクトの方がクラスより汎用性の高い概念だからです。

「このオブジェクトをクラスとして使う」と取り決めれば、もうそれがクラスです。クラスは名前をUpperCamelCaseにするのが慣例ですね。

// Animalクラス
var Animal = {
  // member
  name: "動物"

  // method
, breathe: function(){
    alert("すーはー");
  }

, sayName: function(){
    //メソッド内ではthisがオブジェクト自身を指す
    alert(this.name);
  }
};

オブジェクトからオブジェクトを作る(インスタンス化)

JavaScriptではクラスがないので、クラスからインスタンスを作ることはできません。オブジェクトからオブジェクトを作ります。冒頭のobject関数はこれを簡単に行うための関数です。

var a = object(Animal);

a.name; //"動物"
a.breathe(); //"すーはー"がalertされる

単純にオブジェクトをコピーしてしまうと、メソッドや定数などの共有可能なものまでコピーすることになり無駄なメモリ消費が発生します。しかしobject関数はJavaScriptのプロトタイプ機構を使ってオブジェクトを作るため、効率がよくなります。

単一継承

他の言語では「あるクラスを継承して別のクラスを作る」ということができますが、JavaScriptでは先述の「インスタンス化」と全く同じ操作で単一継承が実現できます。

var Dog = object(Animal); //これで継承が完了
Dog.name = "犬"; //上書きできる
Dog.bowwow = function(){ alert("わんわん!") }; //追加できる

//---こう書いても同じ-----------
var Dog = object(Animal, {
  name: "犬"
, bowwow: function(){ alert("わんわん!") }
});

ここで作ったDogはAnimalを継承した状態になっています。

// 1. AnimalとDogは別々のオブジェクトです。
alert(Animal === Dog); // false

// 2. DogはAnimalのメンバーを全て持っています。
Dog.breathe(); // "すーはー"

// 3. Dogはメンバーを変更することができます。
//    変更した内容はAnimalに影響を与えません。
Dog.breathe = function(){ alert("くーん"); };
Dog.breathe(); // "くーん"
Animal.breathe(); // "すーはー"

// 4. Animalを変更すると、Dogも影響を受けます。
Animal.eat = function(){ alert("もぐもぐ") }; //Animalに追加した
Dog.eat(); //Dogにもeatが追加されているので、"もぐもぐ" がalertされる

Animal.breathe = function(){ alert("すーはーすーはー") };
Dog.breathe(); // "くーん" 
// Dog側ですでに上書きしているので、
// Animal側を書き換えても影響を受けない

// 5. Dog側で行った変更をdeleteすると、Animalの内容に戻ります
delete Dog.breathe;
Dog.breathe(); // "すーはーすーはー"

もちろんDogもobject関数を使ってインスタンス化するように使えます。

var d = object(Dog);

多重継承

正確に言うと、JavaScriptは多重継承をサポートしていません。しかし、関数をコピーすることができるので、多重継承のようなものを実現できます。ただし、JavaやPHPが実装していないことにも見られるように、多重継承は色々と問題があります。考えなしに使うのではなく、Mix-In的に使うのがよい作法です。ここでは詳細は割愛します。

// Wingクラス
var Wing = {
  fly: function(){ alert("ぱたぱた"); }
};


// Animalを単一継承し、WingをmixinしてBirdクラスを作る
var Bird = object(Animal, Wing, {
  name: "鳥"
});


// Animalを単一継承し、WingをmixinしてBatクラスを作る
var Bat = object(Animal, Wing, {
  name: "こうもり"
});

object関数は第一引数を継承のベースとして使い、そのあとの引数は出来上がった新しいオブジェクトへコピー(mixin)されます。引数は可変長であり、あとに書いた方が優先(上書き)されます。


少し長くなってきたので記事を分割して、残りは次回にします。

つづきはこちら。
[newを封印して、JavaScriptでオブジェクト指向する(2)]

参考: JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス
JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス [大型本] / Douglas Crockford (著); 水野 貴明 (翻訳); オライリージャパン (刊)

冒頭のobject関数はプロトタイプ的継承という名前で知られているものを少し改良したものです。おそらくオリジナルはCrockford卿のこの記事です。
Prototypal Inheritance in JavaScript

JavaScriptの最新記事