愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

JavaScriptのnewって何?一体何なの?という話

マクラ

JavaScriptを使っている人なら知っていることだろうけど、JavaScriptはプロトタイプベースのオブジェクト指向を採用しているので「クラス」がない。オブジェクトしかない。

でも、組み込みの演算子としてnewがあって、それを使って

var obj = new Object();

とか書けちゃう。クラスってものはJavaScriptにはないはずなのに、new ClassName();と書くとあたかもClassNameクラスのオブジェクトのインスタンスが生成され、それが返ってくるかのような挙動をしている。

これは気持ち悪い。言語仕様としてはクラスは本来存在しないのに、クラスのようなものが導入されている。まともな神経を持った人間なら、一体new演算子って何なの?という疑問を持つのが当たり前である。「{}はnew Object()のシンタックスシュガーです」とか言ってふんぞり返っている場合ではない。

んで、実験してみた。実行環境はRhino 1.7 release 2。

実験1 newがないとだめな場合

コード
function Rectangle(w, h) {
  this.width = w;
  this.height = h;

  this.toString = function() {
    print(this.width + ", " + this.height + "\n");
  }
}

var r1 = new Rectangle(2, 3);
r1.toString();  // *1

var r2 = Rectangle(2, 3);
r2.toString(); // *2
実行結果
2, 3

js: uncaught JavaScript runtime exception: TypeError: Cannot call method "toString" of undefined
かるく解説

いわゆる、JavaScriptでクラスを定義する、といったときに参考書とか解説サイトで初めに書いてある方法でコンストラクタ関数を定義した場合。
newが本当にただのオマケで、あってもなくてもいいものならr1.toStringとr2.toStringの結果は同じになるはず。しかし、r2.toStringは「そんなメソッドねーよ」と言われてコケているので、少なくともnewが何事かをしているのは分かる。

実験2 newがあってもなくてもいける場合

コード2
function Rectangle(w, h) {

  var toString = function() {
    print(this.width + ", " + this.height + "\n");
  };

  return {
    width: w,
    height: h,
    toString: toString
  }
}

var r1 = new Rectangle(2, 3);
r1.toString();  // *3

var r2 = Rectangle(2, 3);
r2.toString();  // *4
実行結果
2, 3

2, 3
かるく解説

こちらは、オブジェクトを最終的にreturnする関数としてコンストラクタを定義した場合。こちらの場合はnewをつけまいがつけようが同じ結果になるので、「newいらねんじゃね?」といえないこともない。

まとめてちゃんと解説

そもそもnewって何やねん、ということが以下のサイトに書いてある。

http://nanto.asablo.jp/blog/2005/10/24/118564

JavaScript における new 演算子の動作は大まかにいって以下のとおりである。(new F() とした場合。)

1. 新しいオブジェクトを作る。
2. 1 で作ったオブジェクトの ||Prototype|| 内部プロパティ (__proto__ プロパティ) に F.prototype の値を設定する。 
※F.prototype の値がオブジェクトでないのなら代わりに Object.prototype の値を設定する。
3. F を呼び出す。このとき this の値は 1 で作ったオブジェクトとし、引数には new 演算子とともに使われた引数をそのまま用いる。
4. 3 の返り値がオブジェクトならそれを返す。そうでなければ 1 で作ったオブジェクトを返す。

ということになっている(JavaScript: The Good Partsにもnewに関して同様の記述がある)。ちょっと分かり辛いかもしれないが、今回の話に関係のある部分を抜き出すと「new演算子経由でコンストラクタ関数を呼び出した場合、関数内のthisの値は新しく生成されたオブジェクトになる」「コンストラクタ関数の返り値がオブジェクトであった場合、new演算子はだまってそれを返す」ということ。

むりくり実装するとこんな感じか。ちなみに__proto__プロパティがある実装の場合。

Object.prototype["new"] = function() {
  var object = {};

  var proto = (typeof this.prototype === "object") ? this.prototype : Object.prototype;
  object.__proto__ = proto;

  var newObject = this.apply(object, arguments);

  return (typeof newObject === "object") ? newObject : object;
};

ちなみにObject.prototype.newと素直に書いてないのは、newはキーワードなのでそういう書き方をすると「ダメよ!」と怒られるから。


実験1の*1がちゃんと出力されるのはまあ当たり前として、*2がちゃんと動かない(Rhinoちゃんが例外を投げている)のは、そもそもコンストラクタとは単なる関数である、ということを考えれば当たり前田のクラッカーである。

function Rectangle(w, h)はグローバルスコープにある普通の関数なので、それを*2の一行前のように普通に関数として呼び出せばthisはグローバルオブジェクト(一般的なブラウザではwindow)になる。なので、*2の一行前のコンストラクタ関数?呼び出しは結局グローバルオブジェクトをwとhとtoStringというプロパティで汚染する(!)とんでもないコードなのである。

newをつけて呼び出した場合、関数内のthisは新しく生成されたオブジェクトに置き換わるので特に問題ない、というわけである。

実験2の*4のメソッド呼び出しがなぜ失敗しないかというと、コンストラクタ関数の呼び出しがnewのやっていることの4番に引っかかっているからである。つまりコンストラクタ関数がオブジェクトを返しているから、newはそれをそのまま返す。この場合、newをつけた場合の呼び出しとつけない場合の関数呼び出しは全く同じ結果を返すことになるので、*3と*4の呼び出しは結果としてどちらも問題なく行われる。

まとめ

JavaScriptにはコンストラクタ関数なんてものは実はなく、newが裏でよろしくやっているだけ。言語そのものとしてはクラスなんてものはないのだから、個人的には実験2のようなコンストラクタ定義の方が魔術がないぶん分かりやすい、気もする。

補記

「newはいらない」という記事ではなく「newの挙動を知ろう」というつもりで書いたんだけど、確かに誤解を招く記事でもあるので補記。コード2のようにオブジェクトを返すコンストラクタ関数を書くと、newの挙動その1で作られたオブジェクトが最終的に返されないため、prototype継承が行われなくなる。つまりこういうこと。

function Shape(n) {
  this.name = n;
}

function Rectangle1(w, h) {
  return { width: w, height: h };
}

function Rectangle2(w, h) {
  this.width = w;
  this.height = h;
}

Rectangle1.prototype = new Shape("shape");
Rectangle2.prototype = new Shape("shape");

var r1 = new Rectangle1(1,2);
var r2 = new Rectangle2(1,2);

print(r1.name); // -> "undefined"
print(r2.name); // -> "shape"


継承したつもりができていない、残念な結果に。この場合はnewがあるなしに関わらず、Rectangle1()を呼び出した際にprototype継承は行われない。


Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方

Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方