オブジェクトのデフォルト値(valueOfとtoStringの関係)について
この記事の内容はすべてECMAScript5に基づいているのでIE8などでは違う結果になります
以前書いた記事で{toString : valueOf}という記述の意味について考えたのですが、コメントで間違いを指摘して頂いたので改めて書きたいと思います。
今回は簡単のために以下のコードで見ていきます。
var a = { valueOf:function(){ return false; }, toString:valueOf }; document.write(Number(a)); document.write("<br>"); document.write(String(a));
aという変数にvalueOfとtoStringの2つのメソッドを持つオブジェクトをリテラルで設定し、そのaをそれぞれ文字列、数値として評価した場合に表示される内容を確認するコードです。
これを実行すると
0 false
となります。
数値として評価した場合はまずvalueOfが呼び出されfalseが返ってくるので、falseを数値に変換した結果0と表示されています。
数値として評価した場合には特に疑問もないのですが文字列として評価した場合の流れは見た目よりかなり複雑です。
まず
toString:valueOf
という記述で指しているvalueOfが何なのかです。このvalueOfは直前に定義しているメソッドのvalueOf(a.valueOf)ではありません。Javascriptでオブジェクトをリテラルで記述する場合その{}内でほかのプロパティを参照することはできないからです。
//このようには書けない var a = { methodA : function(){}, methodB : methodA }; //methodAは定義されていません。というエラーになる
以前の記事ではオブジェクトのリテラル表記のなかで参照された未定義の変数はObject.prototypeの同名メソッドを参照するようだ、と書いていたのですがこれは厳密に言えば正しくありませんでした。正確には次の流れでObject.prototypeのメソッドが参照されます。
valueOfはコード内でvarをつけて宣言された変数ではないため、まずグローバル名前空間が探されます。Javascriptにおいてグローバル変数はwindowオブジェクトの同名プロパティなのでwindow.valueOfを参照するということになります。
windowオブジェクトはObject.prototypeを継承しているため、プロトタイプチェーンをたどってObject.prototype.valueOfが参照されます。以上がこのvalueOfがObject.prototype.valueOfを参照する流れでした。これが前回の記事の1つ目の間違いです。
2つ目の間違いは前回の記事で「もう一つの謎」として書いていた、なぜObject.prototype.valueOfを参照しているはずのtoStringなのに実行してみると直前に定義したvalueOf(上の例でいえばa.valueOf)を呼び出すのかということについてです。
上の例でも文字列として評価した場合にfalseが表示されていますがfalseはあくまでもa.valueOfの結果でありObject.prototype.valueOfとは関係ないはずです。
以前の記事ではObject.prototype.valueOfはひょっとしたら呼び出されるたびに「自分がオーバーライドされていないかどうか」確認しているんじゃないか、と書いていたのですがこれは全くの間違いでした。
ここでa.valueOfが呼び出されるのはECMAScript(Javascript)の仕様で決められているtoStringとvalueOfの関係に理由があります。
ECMAScript5.1の仕様の「8.12.8 [ [DefaultValue] ] (hint)」という項目に次のように書かれています。
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf
(はてな記法の都合で斜体が使えないのでその部分を「」でくくって表しています) オブジェクト「O」の内部メソッドである[[DefaultValue]]が文字列というヒントを伴って呼ばれた場合は以下の手順で処理される: 1. オブジェクト「O」の内部メソッド[[Get]]に"toString"という文字列を与えて実行した結果を「toString」とする。 2. もしIsCallable(「toString」)がtrueであれば、 a. 「O」をthisとし「toString」の内部メソッド[[Call]]を引数なしで実行した結果を「str」とする。 b. もし「str」がプリミティブな値であれば「str」が返される。 3. オブジェクト「O」の内部メソッド[[Get]]に"valueOf"という文字列を与えて実行した結果を「valueOf」とする。 4. もしIsCallable(「valueOf」)がtrueであれば、 a. 「O」をthisとし「valueOf」の内部メソッド[[Call]]を引数なしで実行した結果を「val」とする。 b. もし「val」がプリミティブな値であれば「val」が返される。 5. TypeError例外を発生させる。
ポイントは2-bのところで、toStringの戻り値がプリミティブな値でなければその値は返されずvalueOfの評価に移るというところです。
冒頭の例の
toString:valueOf
のvalueOfはObject.prototype.valueOfを指しているのでそれ自体はa.valueOfとは関係がないのですが、Object.prototype.valueOfが返すのは呼び出したオブジェクト自体=プリミティブな値ではないので処理が3.の部分に移った結果a.valueOfが実行されるという流れでした。
この処理については以前の記事を書いた後もイマイチ理解できず釈然としなかったのですがコメントで教えていただいたおかげでやっと腑に落ちました。
ところで上の仕様をよく読むとtoStringが呼ばれた際に
IsCallable(toString)
がfalseならば2.aと2.bの処理を飛ばして3.に移れることになります。
ではIsCallable()がfalseを返すような値はどんなものがあるかなのですがこれは仕様書に表でまとめられています。
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf
これによると内部メソッドCallを持つオブジェクト以外はすべてIsCallable()でfalseとなることがわかります。大雑把にいえば関数オブジェクトでなければ何でもいいということのようです。(たぶん)
つまり冒頭の例の
toString:valueOf
の部分は
toString:1
でも
toString:"ほげ"
でも
toString:{}
でもすべて文字列として評価した場合の結果は同じ"false"になります。なんの意味もない数字とかが書いてあるとややこしくなるだけなので使わないとは思いますが。