`+new Date()` がなぜ UNIX 時間を返すのか調べる旅
+new Date()
がなぜ UNIX 時間(ミリ秒)を返すのか調べる旅のログです。
+
は 単項プラス (+) 。
単項プラス演算子 (
+
) は、オペランドの前に置かれ、そのオペランドを評価し、それが数値以外の場合は数値に変換します。
単項プラスに関する ECMAScript の仕様書がこれ。
- Return ? ToNumber(? GetValue(expr)).
と書いてある。
ToNumber ( argument ) を見てみる。
The abstract operation ToNumber takes argument ...(略)
と書かれていて、どうやら ECMAScript 内部の抽象演算らしい。(GetValue も同様に abstract operation らしかった。こちらはあんま重要な意味がなさそうなので飛ばす。)
ToNumber は引数の型によって変換処理が変わるらしい。
(Boolean なら If argument is true, return 1𝔽. If argument is false, return +0𝔽.
など。)
new Date()
の型は何なんだという感じで、たぶん Object だろうということで進むことにします。
Object 型の場合は↓の変換処理。
Apply the following steps:
- Let primValue be ? ToPrimitive(argument, number).
- Return ? ToNumber(primValue).
ToPrimitive でプリミティブ(?)にしたあとそれを更に念押し toNumber して返してる。
ToPrimitive ( input [ , preferredType ] ) を見てみる。
内容を読むと、最初のほうに↓とあり、入力値(今回は new Date()
値)のプリミティブ値変換メソッドをまず取得してきている。(GetMethod って内部で何をしているんだ?)
a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
@@toPrimitive は、Well-Known Symbols と言われる規定の Symbol 型の値らしい。
そして、取得してきたプリミティブ値変換メソッドを呼び出す。引数 hint には、ToPrimitive の第二引数が "number" なので "number" を指定する。なんだか number な値に変換してくれそうな気がしてきた。
iv. Let result be ? Call(exoticToPrim, input, « hint »).
それで、new Date()
値のプリミティブ変換メソッドって一体何なのか。
ところで ECMAScript Language Types という概念があった。
これこそが我々が JavaScript コードで触れる値の型のことらしい。Undefined, Null, Boolean, Number, String, Symbol, BigInt, Object がある。さっき toNumber のところで「引数の型によって変換処理が変わるらしい」と言ったが、この「引数の型」は ECMAScript Language Types のことでよさそう。
Object のセクションに Well-Known Intrinsic Objects という規定の組み込みオブジェクトが書かれており、ここ Date もいた!
The Date Constructor を見てみる。
ここに Date.now() とか Date.prototype.getDate() とか Date.prototype.toString() とか Date.prototype.valueOf() とかが定義されている。
ここの最後に Date.prototype [ @@toPrimitive ] ( hint ) というのがある!
hint って引数も取るみたいだし、toPrimitive でやってる Call(exoticToPrim, input, « hint »)
で呼び出そうとしてるのはこれっぽい感じがする。
new Date()
のプリミティブ変換メソッドってこれなのでは?
ToPrimitive で Date.prototype [ @@toPrimitive ] メソッドを取得するのは 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 プロパティへの参照のこと (*)
- Date の prototype プロパティへの参照は、Date の Prototype Object を指し、これ自体もオブジェクトらしい (*)
-
new Date()
値は、Date の Prototype Object である(はず)
最後のオブジェクトの [[Get]] についてはこのあたり↓に書いてあるけど、なんかよくわからなかった。
よくわからないけど 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 ] の実装には↓とある。
- Return ? OrdinaryToPrimitive(O, tryFirst).
引数 O とは this 値のことであり、今回の場合 this 値とは new Date()
のこと。引数 tryFirst には toNumber から ToPrimitive を経て引き継いだ "number" が入る。
OrdinaryToPrimitive ( O, hint ) を見てみると、引数 hint に "number" が指定されてる場合は、引数 O のオブジェクトに valueOf メソッドがあればまずそれを実行して返すと書かれている!(ちなみに "string" が指定されている場合は toString メソッドをまず実行するらしい。)
今回 O とは new Date()
なので、最終的には new Date().valueOf()
を返していた!
まとめ
細かなところをすごく省略すると、こんな感じ。
+new Date()
- →
ToNumber(new Date())
- →
ToPrimitive(new Date(), "number")
- →
Call(Date.prototype[@@toPrimitive], new Date(), "number")
- →
Date.prototype[@@toPrimitive][[Call]](new Date(), ["number"])
- →
OrdinaryToPrimitive(new Date(), "number")
- →
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 の仕様書を読んだのですがいい機会になりました。
(終)
記事として投稿したのでこちらは閉じます。