JavaScriptのnewって本当にいらない子?
先日、「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にはクラスという機能はなくて、JavaやC++のクラスのような見かけを実現する為のコンストラクタ関数と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を使わないとのこと。使わなくても成り立つものなのかな?
サンプルコードと考察
まずは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 で作ったオブジェクトの ||Prototype|| 内部プロパティ (__proto__ プロパティ) に F.prototype の値を設定する。
- F.prototype の値がオブジェクトでないのなら代わりに Object.prototype の値を設定する。
- F を呼び出す。このとき this の値は 1 で作ったオブジェクトとし、引数には new 演算子とともに使われた引数をそのまま用いる。
- 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さんより、ブコメで以下のコメントをいただきました。
思考の流れがすごく勉強になる。
ありがとうございます!でも、実はこれ、思考の流れをそのまま表しているわけではなくて、記事で書いてない部分で、大量の試行錯誤をしています。見た目は違うけど、実際には同じことをしていたりとか、試行する必要性が皆無なことを、言語仕様を理解しきれていないため、何度もやってたりとか。
この記事に掲載している思考の流れは、第三者が見て混乱しないものだけを載せているということだけ注記させていただきます。まだまだわかってないことがたっくさん!