読者です 読者をやめる 読者になる 読者になる

あと味

たくさん情報を食べて、たくさん発信すると、あとになって味わい深い。

JavaScriptのnewって本当にいらない子?

JavaScript

先日、「JavaScriptのオブジェクトについて考察してみた - あと味」を書いてから、chikuraさんからコメントいただいたり、id:dankogaiさんから「404 Blog Not Found:javascript - にはクラスはない」という記事で言及いただいたり、JavaScript: The Good Partsを読み返したりした結果、newについて調べたいという衝動にかられましたので、その調べた結果を書いてみたいと思います。

newを調べようと思ったキッカケを整理

まずは、そのキッカケから整理します。

chikuraさんのコメントより

押さえるべきポイントは、new演算子の際に何が行われるか?だと思うので、こちらのページもぜひ読んでみてください。


JavaScript の new 演算子の意味: Days on the Moon
http://nanto.asablo.jp/blog/2005/10/24/118564


JavaScriptにはクラスという機能はなくて、JavaC++のクラスのような見かけを実現する為のコンストラクタ関数とnew演算子の組み合わせがある為に、ちょっとややこしいことになってます。

ふむふむ。これまで、newがなんたるかと考えたことなんてありませんでしたが、new演算子の際に何が行われるか?が押さえるべきポイントとのこと。さらに、クラスとしての見かけを実現するためにnewがあるそうです。

弾さんの言及記事より

JavaScriptの最大の特徴が、これでしょう。new演算子があるおかげで、あたかも Class があるように見えはしますが、JavaScript においては、オブジェクトは「Class に属する」のではなく「プロトタイプ・オブジェクトという親から生み出される」のです。

Crockford は「JavaScript: The Good Parts」において、new演算子を「悪いパーツ」に分類しているのもそれが理由です。Classに慣れていると実にへんてこに思えますが、プロトタイプ継承というのはまさに生物がやっていることで、「自然」に考えればそれほど奇異でもなく、私自身最近はほとんどnew演算子を使っていません。

JavaScript: The Good Partsでは、new演算子が「悪いパーツ」に分類されている?そう言えば、JavaScript: The Good Partsは購入して読みましたが、「悪いパーツ」は悪いんだから使わないし、見なくていいだろうとメチャクチャな論理で無視していたことを思い出しましたよorz...

しかも、ほとんどnewを使わないとのこと。使わなくても成り立つものなのかな?

JavaScript: The Good Partsを読み返す

P133

new演算子の持つ問題に対するもっとも良い方法は、newをまったく使わないことである。

その他、newを使った時の問題点が書いてあります。

しかし、まったく使わないとはどういったことか。

無性に気になったので、調べてみることにしました。以下、FireBugで調べた結果です。試したい方は、コンソールにコピペしてお試しください。

サンプルコードと考察

まずはnewを使った通常の処理を書く。

ex1
var Blog = function() {
  this.title = 'あと味';
  this.author = 'jdg';
}

var blog = new Blog();
console.log(blog.title + ' writen by ' + blog.author);

// 結果:
// あと味 writen by jdg

予想どおりの動き。

ex2

つぎに、単純にnewを外してみる。

var Blog = function() {
  this.title = 'あと味';
  this.author = 'jdg';
}

var blog = Blog();
console.log(blog.title + ' writen by ' + blog.author);

// 結果:
// blog is undefined

だめでした。まぁ、いけたらこれで話終わっちゃうけど。

ex3

ex2のはただの関数オブジェクトなので、値を返すためにreturnを加えてみる。

var Blog = function() {
  this.title = 'あと味';
  this.author = 'jdg';
  return this;
}

var blog = Blog();
console.log(blog.title + ' writen by ' + blog.author);

// 結果:
// あと味 writen by jdg

よし、うまくいった。これで、newを一切使わず実装ができる。めでたしめでたし。って単純な話ではありません。

ex4

実はこれ、すごく大変なことになってます。さっきの値の確認の処理でthisを除いて実行してみます。

console.log(title + ' writen by ' + author);

// 結果:
// あと味 writen by jdg

JavaScript: The Good Partsの中で、newを使うべきでないとしている理由がこれです。newを付けるべきところに付けないまま実行すると、グローバル空間にプロパティを追加してしまいます。

実は、ex2でも同様の問題が発生してしまっています。

ex5

ex1とex2のthisの値を確認してみます。

var Blog = function() {
  console.log(this);
}

var blog = new Blog();

// 結果:
// Object

ex1のthisの値は、Object。

var Blog = function() {
  console.log(this);
}

var blog = Blog();

// 結果:
// Window

あ、Windowだ。だから、グローバル空間にプロパティを追加してしまうのか。

つまり、ex2は以下と一緒。

var Blog = function() {
  window.title = 'あと味';
  window.author = 'jdg';
}

var blog = Blog();

newがあるのとないのとで、意味が全然変わってしまう。

そもそもnewって何するの?

で、newって何するの?

ということで、chikuraさんにコメントで教えていただいた以下の記事をよく読んでみることにしました。

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

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

なんかPerlのblessっぽい。

たまにブログの記事とかで見かけるけど、__proto__って何?サイ本にも書いてなさげ。いろいろ調べてみると、__proto__はプロトタイプチェーンを辿る時に使うプロパティとのこと。

とりあえず__proto__の処理の部分は端折って、1、3、4に注目すると、新しくオブジェクトを作って、関数を呼び出して、関数のthisの値に新しく作ったオブジェクトを格納して、呼び出す関数の返り値がオブジェクトなら、そのオブジェクトを返して、そうでなければ、新しく作ったオブジェクトを返すとのこと。

再現してみる。

ex8

額面どおり動かしてみる。

var Blog = function() {
  var object = {};
  this = object;
  this.title = 'あと味';
  this.author = 'jdg';
  return object;
}

var blog = Blog();
console.log(blog.title + ' writen by ' + blog.author);

// 結果:
// invalid assignment left-hand side

ですよね。thisに値は代入できないですよね。すいません。

ex9

仕方ないのでthisの部分も無視する。というか仕様にあるとおり、関数の返り値がオブジェクトなら、新しいオブジェクトを作ろうが何しようが、最終的な返り値は、関数で返すオブジェクトになるんだし、thisを無視しても問題なさそう。

var Blog = function() {
  var object = {};
  object.title = 'あと味';
  object.author = 'jdg';
  return object;
}

var blog = Blog();
console.log(blog.title + ' writen by ' + blog.author);

// 結果:
// あと味 writen by jdg

うまくいったー。

グローバル空間が使われてないかもチェック。

console.log(title + ' writen by ' + author);

// 結果:
// title is not defined
// author is not defined

大丈夫そうに見える。これで解決??

せ、先生!問題が発生しました!

解決したと思った矢先に問題発生。

ex10
var Blog = function() {
  var object = {};
  return object;
};
Blog.prototype = {
  title : 'あと味',
  author : 'jdg'
}

var blog = Blog();
console.log(blog.title + ' writen by ' + blog.author);

// 結果:
// undefined writen by undefined

prototypeプロパティに代入した値がとれない。

ex11

__proto__の存在を無視せず、ECMAScriptの仕様どおり、新しいオブジェクトの__proto__に呼び出し元関数のprototypeをコピーして確認してみる。

var Blog = function() {
  var object = {};
  object.__proto__ = Blog.prototype;
  return object;
};
Blog.prototype = {
  title : 'あと味',
  author : 'jdg'
}

var blog = Blog();
console.log(blog.title + ' writen by ' + blog.author);

// 結果:
// あと味 writen by jdg

うまく動く。やはり、プロトタイプチェーンを辿るときには、prototypeではなく、__proto__が使われるみたい。

ただ、これ、IEでは動きません。__proto__がIEにはないからです。これに関しては、別にIEに落ち度はないように思います。__proto__は、ECMAScriptの標準ではないから。

え?Firefoxちゃん、__proto__でいくの?ECMAちゃんそんなこと言ってた?えぇ!?みんなそうなの??嘘?私だけ仲間はずれ!?

もういいよ!みんな嫌い!絶交よ!私をあなたたちと一緒にしないで!!


IEたん。。。なんかかわいそうにも見える。

Prototype内部プロパティ(Firefox等の__proto__)を、IEで直接書き換える手段は提供されていないようなので、prototypeプロパティを使うなら、newは避けて通れないってことになりました。IEではnew以外でPrototype内部プロパティを書き換えることができません。
そうなると、newを使わないという選択肢は取れなくなるのでしょうか?

ex12

そこで、弾さんの紹介していたプロトタイプ的継承の出番ですよ。ここまで考察して、ようやく有効性が理解できた。

404 Blog Not Found:javascript - プロトタイプ的継承*1

まずは、汎用性を考えずに実装してみる。

var Blog = function() {
  var F = function() {};
  F.prototype = Blog.prototype;
  return new F;
};
Blog.prototype = {
  title : 'あと味',
  author : 'jdg'
}
Blog.prototype.service = 'はてなダイアリー';

var blog = Blog();
console.log( blog.title + ' writen by ' + blog.author + '  powered by ' + blog.service );

// 結果:
// あと味 writen by jdg powered by はてなダイアリー

素晴らしい。

Blogという関数オブジェクトで、return new Blogと値が返せればいいのですが、それはできないみたいです。なので、一旦、新しい関数オブジェクトを用意して、Blogのすべてをコピー。その後、Blogがコピーされた関数オブジェクトを返すことで、return new Blogと同様の処理を実現しています。

で、最終的にnewしたいオブジェクトすべてにこれを実装するのは面倒なので、例にあるとおりのprototype的継承の形にするのがやっぱ便利。

var object = function(o) {
  var F = function() {};
  F.prototype = o.prototype;
  return new F;
};
var Blog = {};
Blog.prototype = {
  title : 'あと味',
  author : 'jdg'
}

var Twitter = {};
Twitter.prototype = {
  username : 'taiju'
}

var blog = object(Blog);
var twitter = object(Twitter);
console.log(blog.title + ' writen by ' + blog.author);
console.log(twitter.username);

// 結果:
// あと味 writen by jdg
// taiju

これでnewから解放された!

まとめ

newがいらないってのは、額面通り受け取るんじゃなくて、newを使わなくて済むように設計しときましょうって意味のようです。*2

追記

id:Layzieさんより、ブコメで以下のコメントをいただきました。

思考の流れがすごく勉強になる。

ありがとうございます!でも、実はこれ、思考の流れをそのまま表しているわけではなくて、記事で書いてない部分で、大量の試行錯誤をしています。見た目は違うけど、実際には同じことをしていたりとか、試行する必要性が皆無なことを、言語仕様を理解しきれていないため、何度もやってたりとか。

この記事に掲載している思考の流れは、第三者が見て混乱しないものだけを載せているということだけ注記させていただきます。まだまだわかってないことがたっくさん!

*1:完全版あり [http://blog.livedoor.jp/dankogai/archives/50662606.html:title:bookmark]

*2:もしくはクロージャを使う