前略
本記事でのJavaScriptは基本的にECMAScript 5のこととし、地の文に出てくる変数 obj はオブジェクトであるとします。
導入
こんな感じのJavaScriptのコードを時々見かけます。
// obj.toString()
str = obj+"";
objを文字列に変換する、というコードですね。
この方法は同じように文字列に変換するobj.toString()やString(obj)に比べてタイプ数が若干短いなどのメリット、メソッドの参照や関数呼び出しが絡まないため速いという都市伝説(都市伝説です。String(obj)の方が速いこともあります[要出典])があります。しかし、果たしてその中身は本当に同じものなのでしょうか?
実験
obj+""がobj.toStringやString(obj)と同じであるか確かめるため、次のようなコードを実行してみます。
var obj = {
toString : function(){
return "toString";
},
valueOf : function(){
return "valueOf";
},
};
console.log(obj.toString()); //=> "toString"
console.log(obj+""); //=> ???
console.log(String(obj)); // ???
さて、コンソールにはどのように表示されたでしょうか?
まともなJavaScript実装であれば、下のように表示されるはずです。
"toString"
"valueOf"
"toString"
上からobj.toString()、obj+""、String(obj)です。
注目すべきは二番目。
"toString"ではなく"valueOf"が返されている、つまりobj.toString()ではなくobj.valueOf()が__暗黙的に__呼ばれたということになります。
Javaをやっている人は首を傾げるかもしれません。JavaScriptはJavaのように文字列の連結時にオブジェクトがあったとき、暗黙的にtoStringが呼ばれるわけではないのです。
ではtoStringはどのようなときに呼ばれるのでしょうか?
次のコードを実行してみてください。興味深い結果が現れるはずです。
var
obj1 = {
toString : function(){
return "obj1 toString";
},
valueOf : function(){
return "obj1 valueOf";
},
},
obj2 = {
toString : function(){
return "obj2 toString";
},
valueOf : function(){
return {};
},
},
obj3 = {
toString : function(){
return {};
},
valueOf : function(){
return {};
},
};
[obj1,obj2,obj3].forEach(function(obj){
console.log(obj+"");
});
obj1はさっきのobjの返す文字列を変えただけ、そしてobj2はvalueOfでobj3は両方のメソッドで{}(空のオブジェクト)を返しています。
結果は以下のようになりました。
"obj1 valueOf"
"obj2 toString"
TypeError : ....
最後は例外が発生しています。
この違いにはどのような意味があるのでしょうか?
色々考えてみてください。
解説
JavaScriptの二項の+演算子には、数値の加算・文字列の連結という二つの役割があります。
この二項の+演算子は、次のようなプロセスで評価されます(もちろん実際の仕様はこんなに単純じゃありません。が、ここで仕様に忠実に説明しても仕方ないので簡単に説明しています。興味ある人は ECMA-262 の11.6 Additive Operatorsの項を参照)。
- 左辺から右辺へ式を評価する。
- それぞれの結果 をプリミティブ値(Number、String、Boolean、Null、Undefined型)に変換する。
-
- どちらかが文字列であれば、 それぞれのプリミティブ値 を文字列に変換して連結する。
- どちらも文字列でなければ、 それぞれのプリミティブ値 を数値に変換して加算する。
また、プリミティブ値に変換するプロセスは次のようになっています(さきに同じ。詳しくはECMA-262 の9.1 ToPrimitiveを参照)。
- 元からプリミティブ値ならそれを返す。
- オブジェクトなら、
-
valueOfプロパティーが関数か確認し、関数であれば実行する。関数でなければ、3に飛ぶ。 - _返り値_がプリミティブ値ならそれを返す。
-
toStringも同様にして、_返り値_がプリミティブ値ならそれを返す。 - プリミティブ値を返せなかったなら
TypeError。
-
この仕様を踏まえて、さっきのコードを見てみましょう。
obj1はまずvalueOfが呼び出され、"obj1 valueOf"が返ってきて、これはプリミティブ値なのでそのまま返され""と連結します。
obj2もvalueOfが呼び出されますが返り値は{}でプリミティブ値ではないので、toStringが呼び出され、今度は返り値が"obj2 toString"とプリミティブ値なので返され連結されます。
obj3ではvalueOfもtoStringも呼び出されますが、どちらも返り値がプリミティブ値ではないのでTypeErrorが投げられる、というわけです。
ですが、この仕様が実際の開発中に問題になることは少ないかと思います。
なぜなら、全てのオブジェクトのデフォルトであるObject.prototype.valueOfが
Object.prototype.valueOf = function(){
return this;
};
のように実装されているため、valueOfがプリミティブ値を返さないため、toStringを自分で実装したときに意図したように呼ばれるからです。
しかし、何らかの理由でvalueOfを実装した場合、obj+""では不具合が生じることがあるでしょう。
ちなみに関数(コンストラクタではない)Stringの場合は、これとは反対にtoStringを呼んでプリミティブ値が返されなければvalueOfを呼び、その値を文字列に変換する、というプロセスとなっています。
var
obj1 = {
toString : function(){
return {};
},
valueOf : function(){
rettrn "valueOf";
},
},
obj2 = {
toString : function(){
return {};
},
valueOf : function(){
return {};
},
};
console.log(String(obj1)); //=> "valueOf"
console.log(String(obj2)); //TypeError!
あとobj.toString()ですが、これは単にobjのtoStringメソッドを呼んでいるだけなので、文字列以外が返ってくる可能性も否定できません。(もちろん、文字列を返すべきですが…
まとめ
-
toStringは__Javaのように__文字列の連結に暗黙的に使用されるメソッドではない。 -
valueOfも__Javaのように__文字列を数値オブジェクトに変換するメソッドではない。 -
obj+""はまずobj.valueOf()を呼び、obj.toString()を呼ばないことがある(がしかし、実は一つだけ例外がある)。 -
String(obj)は間違いなくobj.toString()を呼ぶ。 - つまり
obj+""はString(obj)、obj.toString()の完全な代替とは呼べない。
obj+""は実際の開発でもよく使われるテクニックですが、以上のような問題点も孕んでいることを知っておいて損はないのでしょうか?
付け加えて、必ず文字列を得たい面なら
str = String(obj.toString());
が最も期待する動作に近いのではないかと思います。(もちろん状況にもよりますけどね^^;
感想
JavaScriptが__中途半端にJavaっぽくした__のが裏目に出た、良い例になったような気がします。
あとは、この記事を書くために ECMA-262 に手を出したので、それはそれでいい経験になったかな、と。
はぁ…。
なんて言うか、自由研究みたいな構成だなぁ…。
荒削りの知識ですので、間違い等ありましたらコメントしていただけるとありがたいです。