valueOfとtoStringとToPrimitive

valueOfとtoStringメソッドの水深43cmぐらいの深さの話 - 三等兵のもう少し深いお話。コメント欄に書こうかとも思ったけど、最近ブログ書いてない気がしたのでちゃんと記事にしてみる。

まずは問題です。次のコードを実行したときにtrueかfalseのどちらがalertされるかそれぞれ当ててみてください。

var date = new Date();
var date_string = date.toString();
var date_value = date.valueOf();
alert(date == date_string);
alert(date == date_value);
  1. true, true
  2. false, false
  3. true, false
  4. false, true

(難しい問題ではないと思いますが、)この問題の答えは最後に。

続いて、もっとシンプルな問題です。

var d = new Date('2000/01/1');
alert(d+1);

選択肢は次の3つです。

  1. Sat Jan 01 2000 00:00:00 GMT+09001
  2. 946652400001
  3. [object Date]1

1)はDateをtoStringした値、2)はvalueOfした値、3)はObject.prototype.toString.call(d)の値です。


この問題の答えは1です。これはよくハマるところなので、そういうものだと思っている方も多いと思います。
最初の問題もそうですが、DateのtoStringが文字列を、valueOfがシリアル値を返すことを知っていれば、正解できると思います。

余談ですが、DateをtoStringしたときの結果はブラウザによって微妙に結果が異なります。

  • Firefox/Opera
    • Sat Jan 01 2000 00:00:00 GMT+0900
  • Google Chrome
    • Sat Jan 01 2000 00:00:00 GMT+0900 (Japan Standard Time)
  • Safari(Mac)
    • Sat Jan 01 2000 00:00:00 GMT+0900 (JST)
  • Safari(Windows)
    • Sat Jan 01 2000 00:00:00 GMT+0900 (???? (?W?Ž?))
  • IE
    • Sat Jan 1 00:00:00 UTC+0900 2000

IEさんはともかく、Windows版Safari*1

さて、なぜ1になるのかを詳しくみてみましょう。

まず、変数dはDateのインスタンスであり、オブジェクトです。そのオブジェクトに加法演算子(+)で1を渡しています。
加法演算子の仕様はECMA-262の11.6.1で定められています*2
その前半部分は次のようになっています。ここではAdditiveExpressionは左項、MultiplicativeExpressionは右項と読み替えてしまえばわかりやすいと思います*3

  1. AdditiveExpression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. MultiplicativeExpression を評価。
  4. GetValue(Result(3)) を呼出す。
  5. ToPrimitive(Result(2)) を呼出す。
  6. ToPrimitive(Result(4)) を呼出す。

つまり、d + 1のそれぞれ d と 1 をToPrimitiveという内部処理に渡しています。
ToPrimitiveはつまりは型変換であり、ECMA-262の9.1で定義されています。
このToPrimitiveですが、オブジェクト以外の型についてはなにもしません。つまり1は1のままです。1はもともとプリミティブな値なので当然ですね。
さて、もう一方のdはDateオブジェクトです。なのでプリミティブ化処理が行われます。その内容は次のように定義されています。

Object のデフォルトの値を返す。オブジェクトのデフォルトの値は、オブジェクトの内部メソッド [[DefaultValue]] に選択的ヒント PreferredType を渡して取得される。[[DefalutValue]] メソッドの挙動は、全ての ECMAScript オブジェクトの仕様によって定義される。(セクション 8.6.2.6)

今度は[[DefaultValue]]というのが出てきました。上記に書かれている通り、[[DefaultValue]]はECMA-262の8.6.2.6で定義されています。
これによると、[[DefaultValue]]がStringをヒントに呼び出されるとtoStringが、Numberをヒントに呼び出されるとvalueOfを呼びだすと定義されています。toStringとvalueOfを呼び分けているのはここです。ここ大事ですね。
さて、ヒントとはなんでしょうか。と、これはすでにちょこちょこと出てきていて、加法演算子のNOTEには、次のような記述があります。

NOTEステップ 5 と 6 における ToPrimitive 呼出しではヒントを提供しない。全ネイティブ ECMAScript オブジェクトは Date オブジェクトを除き、ヒントの欠如をヒント Number が与えられたように扱う; Date オブジェクトは、ヒントの欠如をヒント String が与えられたように扱う。

[[DefaultValue]]のNOTEにも同様の内容が記述されています。Date オブジェクトだけはStringをヒント、つまりtoStringが呼ばれると定義されていました。

さて、こんな面倒なことになっている原因はなんでしょうか?それは+が、文字列の連結としての加算演算子と数値の加算としての加算演算子という2つの意味を持ってしまっているからです。-、*、/などは数値演算であることが明確なので、そもそもToPrimitiveではなくToNumberが呼ばれ、ToNumberは(オブジェクトに対して)ToPrimitiveをNumberをヒントに呼び出すことになります。

また、加算演算子以外にも、==と!=(抽象的等価比較)についても場合によってヒントなしのToPrimitiveが呼ばれることがあります。具体的には、オブジェクトとプリミティブ値が比較された場合です。

最初の問題に戻ってみましょう。

var date = new Date('2000/01/1');

var date_string = date.toString();
// Sat Jan 01 2000 00:00:00 GMT+0900
alert(date == date_string);

dateはオブジェクトで、date_stringは文字列なので、11.9.3 抽象的等価比較アルゴリズムのステップ21からToPrimitive(date)が呼ばれ、その中でtoStringが呼ばれます。結果、dateとdate_stringは一致します。

var date_value = date.valueOf();
// 946652400001
alert(date == date_value);

dateはオブジェクトで、date_valueは数値なので、同じく11.9.3 抽象的等価比較アルゴリズムのステップ21からToPrimitive(date)が呼ばれ、その中でtoStringが呼ばれます。dateは文字列になり、date_valueは数値なのでこちらは一致しません。
というわけで、最初の問題の答えは3でした。

おまけで、以前話題になったwtfjsから、こちらを読み解いてみると面白いと思います。

[] == ![] // true

http://wtfjs.com/2010/02/15/careful

*1:windows版リリース当初からバグッたまま

*2:ホントは5th editionも参照するべきなんだけど、ざっと見た感じ特に変更はなさそうなので割愛

*3:左項、右項って日本語はなさそうですが、伝わると思います