🧭

+new Date() はなぜ UNIX 時間を返すのか

2022/06/11に公開

+new Date() がなぜ UNIX 時間(ミリ秒)を返すのか調べる旅の記録です。

単項プラス (+)

+単項プラス (+)

単項プラス演算子 (+) は、オペランドの前に置かれ、そのオペランドを評価し、それが数値以外の場合は数値に変換します。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Unary_plus

+1 // -> 1
+'' // -> 0
+'100' // -> 100
+'hoge' // -> NaN
+true // -> 1
+false // -> 0
+{} // -> NaN
+[] // -> 0
+[99] // -> 99
+[1,2] // -> NaN

単項プラスに関する ECMAScript の仕様書

仕様書がこれ → https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-unary-plus-operator

UnaryExpression : + UnaryExpression

  1. Let expr be the result of evaluating UnaryExpression.
  2. Return ? ToNumber(? GetValue(expr)).

と書いてある。
ここには単項プラスがランタイムでどのように評価されるかが書かれていて、

+(単項式) のとき、単項式を UnaryExpression とする

  1. UnaryExpression の評価結果を expr とする。
  2. さらに ToNumber(? GetValue(expr)) の結果を返す。

みたいな感じ。

ToNumber

ToNumber ( argument ) を見てみる。これは名前のとおり、引数を取って Number 型の値に変換するメソッド。

The abstract operation ToNumber takes argument ...(略)

と書かれていて、どうやら ECMAScript 内部の抽象演算らしい。(GetValue も同様に abstract operation らしかった。こちらはあんま重要な意味がなさそうなので飛ばす。)

変換処理は引数の型によって変わるらしい。(Boolean なら If argument is true, return 1𝔽. If argument is false, return +0𝔽. など。)

new Date() の型は何なんだという感じで、たぶん Object だろうということで進むことにします。

Object 型の場合は↓の変換処理。

Apply the following steps:

  1. Let primValue be ? ToPrimitive(argument, number).
  2. Return ? ToNumber(primValue).

ToPrimitive でプリミティブ(?)にしたあとそれを更に念押し toNumber して返してる。

ToPrimitive

ToPrimitive ( input [ , preferredType ] ) を見てみる。

内容を読むと、最初のほうに↓とあり、入力値(今回は new Date() 値)に対応するプリミティブ値変換メソッドをまず取得してきている。(GetMethod って内部で何をしているんだ?)

a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive[1]).

そして、取得してきたプリミティブ値変換メソッド exoticToPrim を呼び出す。

iv. Let result be ? Call(exoticToPrim, input, « hint »).

引数 hint には、ToNumber から ToPrimitive の第二引数に渡しているのが "number" なので "number" が入る。なんだか number な値に変換してくれそうな気がしてきた。

それで、new Date() 値のプリミティブ変換メソッドって一体何なのか。

ECMAScript Language Types

ところで ECMAScript Language Types という概念があった。

これこそが我々が JavaScript コードで触れる値の型のことらしい。Undefined, Null, Boolean, Number, String, Symbol, BigInt, Object がある。

さっき toNumber のところで「引数の型によって変換処理が変わるらしい」と言ったが、この「引数の型」は ECMAScript Language Types のことでよさそう。

これの Object のセクションに Well-Known Intrinsic Objects[2] という規定の組み込みオブジェクトが書かれており、ここ Date もいた!

The Date Constructor

The Date Constructor を見てみる。

ここに Date.now() とか Date.prototype.getDate() とか Date.prototype.toString() とか Date.prototype.valueOf() とかが定義されている。

ここの最後に Date.prototype [ @@toPrimitive ] ( hint ) というのがある!

toPrimitive で呼び出していた、入力値に対応するプリミティブ変換メソッドはこれっぽい感じがする。そのメソッドの呼び出しは Call(exoticToPrim, input, « hint ») だし、hint って引数名からしてもそうっぽい。
new Date() のプリミティブ変換メソッドってこれなのでは?

GetMethod

ところで、ToPrimitive でプリミティブ変換メソッドを取得するのは GetMethod でやってた。new Date() のプリミティブ変換メソッドが本当にあれだったのか確認のためにもこれを見てみる。

GetMethod は、引数 V (ECMAScript Language 値) と P (プロパティキー) を取って関数オブジェクトを返すもの。

GetMethod の内部では GetMethod(V, P)GetV(V, P)O.[[Get]](P, V) の順で呼び出している。
今回の場合、具体的には GetMethod(new Date(), @@toPrimitive)GetV(new Date(), @@toPrimitive)%Date.prototype%.[[Get]](@@toPrimitive, new Date()) という感じ(たぶん)。

  • %Date.prototype% は、組み込みオブジェクト Date の prototype プロパティへの参照のこと[3]
  • Date の prototype プロパティへの参照は、Date の Prototype Object を指し、これ自体もオブジェクトらしい[4]
  • new Date() 値は、Date の Prototype Object である

最後のオブジェクトの [[Get]] についてはこのあたり↓に書いてあるけど、なんかよくわからなかった。

https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-property-attributes

結論

よくわからないけど Date.prototype [ @@toPrimitive ]new Date() 値のプリミティブ値変換メソッドだとする。

だから、toPrimitive でやっていた Call(exoticToPrim, input, « hint »)Call(Date.prototype[@@toPrimitive], new Date(), "number") って感じだった。かなり new Date() 値をプリミティブ値、それも number 値に変換しそうになってきた。

さらに、Call ( F, V [ , argumentsList ] ) を読むと最終的にやってるのは F.[[Call]](V, argumentsList) だった。今回の場合 Date.prototype[@@toPrimitive][[Call]](new Date(), ["number"]) って感じ。[[Call]] は第一引数に this 値、第二引数にメソッドの引数の配列を取って呼び出す。つまり new Date() を第一引数を this として Date.prototype[@@toPrimitive] を呼び出している。

Date.prototype [ @@toPrimitive ] の内容には↓とある。

  1. Return ? OrdinaryToPrimitive(O, tryFirst).

引数 O とは this 値のことであり、今回の場合 this 値とは new Date() のこと。引数 tryFirst には toNumber から ToPrimitive を経て引き継いだ "number" が入る。

OrdinaryToPrimitive ( O, hint ) を見てみると、引数 hint に "number" が指定されてる場合は、引数 O のオブジェクトに valueOf メソッドがあればまずそれを実行して返すと書かれている![5]
今回 O とは new Date() なので、最終的には new Date().valueOf() を返していた!

(雑説明)

細かなところをすごく省略すると、こんな感じ。

  1. +new Date()
  2. ToNumber(new Date())
  3. ToPrimitive(new Date(), "number")
  4. Call(Date.prototype[@@toPrimitive], new Date(), "number")
  5. Date.prototype[@@toPrimitive][[Call]](new Date(), ["number"])
  6. OrdinaryToPrimitive(new Date(), "number")
  7. new Date().valueOf()

あとがき

+new Date() するのと同じように dayjs でも +dayjs() して楽をして UNIX 時間を取得してたところ、「dayjs() の挙動が将来的に変わったらこういうとこでバグりそうだから、ドキュメントにも Unix Timestamp (milliseconds) を返すとちゃんと書かれている dayjs().valueOf() を使いませんか?」との意見をチームの人から貰いました。
確かにそうかもなと思ってこれは変更しようとしたけど、そもそもどうして dayjs()new Date() を単項プラスで「数値に変換」すると(2022年の 2022 でも 6月の 5 でもなく)UNIX 時間を返すんだろうと疑問に思いました。
調べてみると結局最終的には valueOf メソッドを呼び出していて、dayjs().valueOf() はまさにこの為に用意してたんじゃないか?!と感動しました。(単に new Date() に合わせただけかもしれない、あるいはオブジェクトの valueOf メソッドは数値を返し toString メソッドは文字列を返すというのが常識なのかもしれないですが。)
今回はじめて ECMAScript の仕様書を読んだのですがいい機会になりました。

(終)

追記

所属している stand.fm 社ではエンジニアを募集しています。
スマホアプリとサーバーを JavaScript (TypeScript) で開発しています。
よければ覗いてみてください↓。
https://corp.stand.fm/recruit

脚注
  1. @@toPrimitive は、Well-Known Symbols と言われる規定の Symbol 型の値らしい。 ↩︎

  2. https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-well-known-intrinsic-objects ↩︎

  3. https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-object-type:~:text=Within this specification a reference,any ECMAScript code being evaluated. ↩︎

  4. https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-properties-of-the-date-prototype-object ↩︎

  5. ちなみに "string" が指定されている場合は toString メソッドをまず実行するらしい。 ↩︎

Discussion