{toString : valueOf}というオブジェクトリテラル記述の不思議について
先日404 Blog Not Foundの404 Blog Not Found:Algorithm - 0と1を次々と返す簡単なお仕事という記事を読んでいて気になったことがありましたのでそのことについて。
といっても記事の内容そのものとは関係がないのですが、この記事の一番最後に追記として書かれているサンプルコードに下の物があります。
#!/usr/bin/js var flipflop = function(p){ return { p : 0, valueOf : function(){ return this.p = !this.p; }, toString : valueOf }; }; var fl = flipflop(); print(fl); print(fl); print(fl); print(fl); print(uneval(fl));
この中の
toString : valueOf
という部分のことです。
これ一見するとtoStringメソッドに直前に定義したvalueOfメソッドをオーバーライドしているように見えるのですが、Javascriptのオブジェクトをリテラル(JSON式の書き方)で表記する場合、その{}内で他のプロパティを参照することはできないはずです。
//このようには書けない var a = { methodA : function(){}, methodB : methodA };
この例ですとmethodAは定義されていません。というエラーになります。
上の例はコマンドライン向けに書かれているのでブラウザで動作できるようにここからは下の例でみていくことにします。
var flipflop = function(p){ return { p: 0, valueOf: function(){ return this.p = !this.p; }, toString: valueOf }; }; var fl = flipflop(); document.write(fl.toString()); document.write(fl.toString()); document.write(fl.toString()); document.write(fl.toString());
このコードはChromeやFireFoxでは動きますがIE8では動きませんでした。IE8はやはり「valueOfは宣言されていません。」というエラーになります。
ではChromeやFireFoxが
toString:valueOf
で指しているvalueOfは一体なんなのでしょうか。結論から言いますと、これはどうもObject.prototype.valueOfのようです。つまりChromeやFireFoxではオブジェクトの表記内で未定義の変数が参照された場合、暗黙のうちにObject.prototypeのメソッドを参照するようです。確認のため下のコードを実行してみるとたしかに動きます。
Object.prototype.hoge = function(){return "hoge"}; var test = { toString:hoge }; document.write(test);//hogeが表示される
でも、そうなるとhogeという変数も定義されていた場合はどうなるんだ?となります。そこで下のコードを試してみると、変数の方が参照されていました。
var hoge = function(){return "変数のhoge"}; Object.prototype.hoge = function(){return "プロトタイプのhoge"}; var test = { toString:hoge }; document.write(test);//"変数のhoge"が表示される
ここまでをまとめると、Chrome、FireFoxではオブジェクトのリテラル表記のなかで変数が参照された場合その変数が定義されていればそのままその変数が参照され、もし未定義であればObject.prototypeの同名メソッドが参照される、ということのようです。
これで1つめの謎は解けたのですが
toString:valueOf
にはまだ謎があります。上で書いたように定義時点ではtoStringが参照しているのはあくまでObject.prototype.valueOfであるはずなのになぜ実行して見ると直前に定義している
valueOf : function(){ return this.p = !this.p; },
が参照されているのかということです。たしかにvalueOfはオーバーライドされていますが、定義時にtoStringが参照した時点ではまだされていなかったはず・・・。私の知識では変数やプロパティが何を参照するかはすべて定義時に決定されるはずで呼び出し時にそれが変わる事はない、というものだったのでこれはどうも腑に落ちない挙動なのです。ひょっとするとObject.prototypeのメソッドは呼び出されると「自分がオーバーライドされていないかどうか」を毎回確認しているのでしょうか。つまり
Object.prototype.valueOf = function(){ if(this.hasOwnProperty("valueOf")){ return this.valueOf(); }; /* * 実際の処理 */ };
という処理になっているのだとしたら納得がいきます。ではこれはvalueOfメソッドなどの組み込みメソッドがそのような実装になっているのかObeject.prototype下のメソッドすべてが自動的にそのように扱われるのかどちらなのでしょうか。これを確認するために次のコードを試してみました。
Object.prototype.hoge = function(){ return "プロトタイプのhoge"; }; var test = { hoge: function(){ return "testのhoge"; }, toString: hoge }; document.write(test);//"プロトタイプのhoge"が表示される
どうやら自動的ではないようです。組み込みメソッドのようなprototype拡張をするには
Object.prototype.hoge = function(){ if(this.hasOwnProperty("hoge")){ return this.hoge(); }; return "プロトタイプのhoge"; }; var test = { hoge: function(){ return "testのhoge"; }, toString: hoge }; document.write(test);//"testのhoge"が表示される
と書くとよさそうです。
ここまでの話はすべてIE8では動かないのでECMAScript5の仕様なのかなと思って本家をみてみたのですが該当する箇所がわかりませんでした(^_^;)
8/10追記。このエントリの内容に間違いがあることを教えていただいたので新たな記事を書きました→オブジェクトのデフォルト値(valueOfとtoStringの関係)について - 主にプログラムを勉強するブログ
詳しくはコメント欄をご覧ください。