newを封印するべき4つの理由

Posted by Hiraku on 2011-05-13

ちょっと勢いに任せて書いちゃったので攻撃的な文章になってます。。 すごくわかりやすい解説記事を頂いたので、こちらも必読です。
new を不当に貶める陰謀と JavaScript におけるクラスの継承構造の話 - vivid memo
(追記:2011/5/15)

前の記事「newを封印して、JavaScriptでオブジェクト指向する」が思いのほか反響が大きかったので、調子に乗って補足を書いてみますよ。

ブコメへの返信

「無用な複雑化に見える。」「俺俺オブジェクト指向な気がする。」といったご意見もいただいたんですが、普通たった10行の関数でオレオレオブジェクト指向なんて作れないですよ…。JavaScriptの内部機構をほとんどそのまま使っているからこそ、この行数で済むのです。

それに、このプロトタイプ的継承の考え方をDouglas Crockfordが編み出したのは、もう5年も前の話です。ECMAScript5からObject.createという名前で公式に採用されたことからもわかる通り、newを使う古典的なスタイルより、プロトタイプ的継承を使うパターンの方がモダンであり主流になりつつあります。(object関数は多重継承用に拡張しちゃっていますが…)

ちょっと言い訳めいてしまいました。前回の記事では、私が思う「こうするのが一番わかりやすい」という書き方をまとめました。では逆に、標準的な書き方に従ってnewを使うように書いた場合どうなるか、要するにnewをさんざんdisろうというのが、今回の趣旨です。少し主観が混じっているかもしれませんが、ご了承くださいませ。

newキモイ1:newを付けなくてもコンストラクタが動作してしまう

例えば、長方形の面積を計算するクラスをnewを使って作ってみましょう。

function Rectangle(x, y) {
  this.x = x;
  this.y = y;
  alert(x + " x " + y + "の長方形を作りました");
}
Rectangle.prototype = {
  getArea: function(){ return this.x * this.y }
};

//使い方の例
var rec1 = new Rectangle(3, 4);
alert(rec1.getArea()); //12

newを使うことを想定するクラスの書き方を私はうまく説明する自信が無いので、詳しくは書きませんが、ごくごくシンプルなクラスなので読めると思います。

見てわかる通り、Rectangleはクラス専用の文法で書いてあるわけではなく、ごく普通の関数です。なのでnewを付けずに呼ぶことができます。そしてnewをつけずに呼んでしまった場合の挙動が、もう非常に気持ち悪いことになります。

Rectangle(3,4); //newつけわすれ
print(x); //3
print(y); //4

newを付けずに呼んだ場合、関数内のthisはグローバルオブジェクトを指します。おかげでグローバル変数にxとyが追加されてしまいました。グローバル空間に既にxとyが存在する場合、それらの変数は破壊されてしまいます。

一応、最近の処理系ではstrictモードでこの挙動を抑制することができるようになっています。

処理系のバージョンに依存しないで行える対処方法は、以下のようなガード節を書いておくことです。

function Rectangle(x, y) {
  if (!(this instanceof Rectangle))
    return new Rectangle(x, y);

  this.x = x;
  this.y = y;
  alert(x + " x " + y); //動作確認のためのalert
}
Rectangle.prototype = {
  getArea: function(){ return this.x * this.y }
}

でもクラスを作るたびにこんな定型文を書かないといけないなんて、アホらしいと思いませんか?

newキモイ2:継承を考慮する必要がある

JavaScriptではインスタンス化と単一継承が同義です。そのため、インスタンス化するときだけでなく、継承させるときも考慮してコンストラクタを書かないといけません。

先ほどの長方形クラスを継承して、正方形クラスを作ることを考えてみます。素直に書くとこうなります。

function Square(x) {
  Rectangle.call(this, x, x);
}
Square.prototype = new Rectangle;

しかしこう書いた場合、new RectangleしたときにRectangleのコンストラクタが勝手に動いてしまうため、alert()が呼ばれてしまいます。純粋に継承だけさせようと思うと、工夫が必要になります。

やり方はいくつかあります。

// 継承を考慮したコンストラクタ

//1. argumentsの数で見る
function Rectangle(x, y) {
  if (arguments.length == 0) //継承させようとしているので、何もしない
    return this;
  ...
}

//2. arguments[0]の型を見る
function Rectangle(x, y) {
  if (typeof arguments[0] == "string" && arguments[0] == "inherit")
    return this;
  ...
}
...
Square.prototype = new Rectangle("inherit"); //継承させるときは特定のキーワードを渡す

//3. そもそもコンストラクタを別メソッドに分けておく
function Rectangle(){}
Rectangle.prototype = {
  init: function(x, y) { ... return this; }
  ...
};
var rec1 = new Rectangle().init(3,4);
...
Square.prototype = new Rectangle;

どんなコンストラクタを書いていようと対応できるように、こんな書き方をすることもあります。

function Square(x) {
  Rectangle.call(this, x, x);
}
Square.prototype = (function(){
  function f(){}
  f.prototype = Rectangle.prototype;
  return new f;
})();

この書き方は、object関数をインライン展開したのと同義です…。それならいっそのこと、常にobject関数を使うようにした方が楽だと思いませんか?

newキモイ3:何をやっているのかわかりにくい

JavaScript自体はプロトタイプベースで作られているくせに、newを使うとクラスベースみたいな書き方をする必要があります。中でやっていることと、外側から見えるインターフェースが違いすぎるのです。おかげで逆にわかりにくくなっています。

newキモイ4:読みづらい

newを生で使う場合、prototypeという長ったらしいキーワードを多用することになります。このせいで、newを前提に書いたクラスは、総じて読みづらくなりがちです。

試しに補助関数を一切使わずに、Penguinクラスを定義してみましょう。

function Animal(){}
Animal.prototype = {
  name: "動物"
, breathe: function(){alert("すーはー")}
, sayName: function(){alert(this.name)}
};

function Wing(){}
Wing.prototype = {
  fly: function(){alert("ぱたぱた")}
};

function Bird(){}
Bird.prototype = new Animal;
Bird.prototype.name = "鳥";
Bird.prototype.fly = Wing.prototype.fly;

function Penguin(){}
Penguin.prototype = new Bird;
Penguin.prototype.name = "ぺんぎん";
Penguin.prototype.fly = function(){
  Bird.prototype.fly.apply(this, arguments);
  alert("ぺんぎんは飛べない…");
};

この程度の単純なものでも、prototypeを10回も書かなければなりません。

一方で、object関数を使って書けばこうなります。

var Animal = {
  name: "動物"
, breathe: function(){alert("すーはー")}
, sayName: function(){alert(this.name)}
};

var Wing = {
  fly: function(){alert("ぱたぱた")}
};

var Bird = object(Animal, Wing, {
  name: "鳥"
});

var Penguin = object(Bird, {
  fly: function(){
    Bird.fly.apply(this, arguments);
    alert("ぺんぎんは飛べない…");
  }
});

こちらの方が読みやすいと思いませんか? これでもnewを使った方がわかりやすい、という人がいたら、私の負けです。どうぞ好きなだけnewを使ってください。。

まとめ

少なくとも俺は、newは気持ち悪いと思う。

ただ、ちゃんと理解した上で使うのはもちろん問題ないと思いますよ。newを生で使った方が関数を呼ぶ回数を減らせるので、パフォーマンスが良くなるというメリットもあります。

keyword: javascript

JavaScriptの最新記事

×

この広告は90日以上新しい記事の投稿がないブログに表示されております。