Closed12

`+new Date()` がなぜ UNIX 時間を返すのか調べる旅

moriyuumoriyuu

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

moriyuumoriyuu

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:

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

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

moriyuumoriyuu

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() 値のプリミティブ変換メソッドって一体何なのか。

moriyuumoriyuu

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

これこそが我々が JavaScript コードで触れる値の型のことらしい。Undefined, Null, Boolean, Number, String, Symbol, BigInt, Object がある。さっき toNumber のところで「引数の型によって変換処理が変わるらしい」と言ったが、この「引数の型」は ECMAScript Language Types のことでよさそう。

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

moriyuumoriyuu

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() のプリミティブ変換メソッドってこれなのでは?

moriyuumoriyuu

ToPrimitiveDate.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]] についてはこのあたり↓に書いてあるけど、なんかよくわからなかった。

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

moriyuumoriyuu

よくわからないけど 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 メソッドがあればまずそれを実行して返すと書かれている!(ちなみに "string" が指定されている場合は toString メソッドをまず実行するらしい。)
今回 O とは new Date() なので、最終的には new Date().valueOf() を返していた!

moriyuumoriyuu

まとめ

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

  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()
moriyuumoriyuu

あとがき

+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 の仕様書を読んだのですがいい機会になりました。

(終)

このスクラップは2022/06/11にクローズされました