🦭

console.logについて理解していますか?

2025/01/02に公開

はじめに

console.log()を使っていますか?
コンソールデバッグをしない派の人もまったく使わないことはないでしょう。

https://developer.mozilla.org/ja/docs/Web/API/console/log_static

// 文字列を渡すと表示される
console.log("Hello, world!"); // Hello, world

わかりやすいですね。

// オブジェクトも表示する
console.log({ name: "Taro" }); // { name: 'Taro' }

オブジェクトも表示します。

では、これはどう表示されるでしょうか。

console.log(new Date())

実は実行環境によって結果が変わります。

// ブラウザ(Chrome)で実行
console.log(new Date()); // Fri Dec 27 2024 14:40:45 GMT+0900 (日本標準時)

// Node.jsで実行
console.log(new Date()); // 2024-12-27T14:30:45.000Z

ちなみに

// nodeで+''を足してみると
console.log(new Date() + ""); // Fri Dec 27 2024 14:40:45 GMT+0900 (日本標準時)

想像通りでしたか?
この記事ではconsole.log()について理解して、これを説明できるようになることをゴールとします。

console.log()の仕様

WHATWG

いわゆるWeb標準のWHATWGでconsole.log()の仕様を見てみましょう。
https://console.spec.whatwg.org/#log

printerが実行され、printerはThe printer operation is implementation-defined. とあります。つまり、実装により振る舞いを変えてよいことが標準である(ややこしい)ということです。
https://console.spec.whatwg.org/#printer

Node

それでは次にNode.jsでのconsoleの仕様を見てみましょう。
https://nodejs.org/api/console.html

引数はutil.format()を通してprintf(3)に渡されるとあります。
util.format()とはなんでしょうか

util.format()

https://nodejs.org/api/util.html#utilformatformat-args

具体的に文字列でないものを渡したらどうなるか、については書かれていません。
オブジェクトを文字列として扱うときにどうするか、これについてはECMAScriptで仕様があります。

暗黙的な型変換について
https://tc39.es/ecma262/#sec-toprimitive

1. If input is an Object, then
   a. Let exoticToPrim be ? GetMethod(input, %Symbol.toPrimitive%).
   b. If exoticToPrim is not undefined, then
      i. If preferredType is not present, then
         1. Let hint be "default".
      ii. Else if preferredType is string, then
          1. Let hint be "string".
      iii. Else,
           1. Assert: preferredType is number.
           2. Let hint be "number".
      iv. Let result be ? Call(exoticToPrim, input, « hint »).
      v. If result is not an Object, return result.
      vi. Throw a TypeError exception.
   c. If preferredType is not present, let preferredType be number.
   d. Return ? OrdinaryToPrimitive(input, preferredType).
2. Return input.

実際に挙動を確認してみましょう。
Chromeで実行してみると次のようになります。

// Symbol.toPrimitiveの存在確認
console.log(
  "Symbol.toPrimitive exists on Date.prototype:",
  Symbol.toPrimitive in Date.prototype,
); // Symbol.toPrimitive exists on Date.prototype: true

// 実際の変換を試してみる
console.log("Direct console.log:", date); // Direct console.log: Thu Jan 02 2025 17:20:18 GMT+0900 (日本標準時)

// 各hintでの動作確認
console.log('hint "string":', date[Symbol.toPrimitive]("string")); //  hint "string": Thu Jan 02 2025 17:20:18 GMT+0900 (日本標準時)
console.log('hint "number":', date[Symbol.toPrimitive]("number")); // hint "number": 1735806018414
console.log('hint "default":', date[Symbol.toPrimitive]("default")); // hint "default": Thu Jan 02 2025 17:20:18 GMT+0900 (日本標準時)

DateオブジェクトとにはtoPrimitiveが存在しています。
console.logで実行されるのは、stringやdefaultでのtoPrimitiveと一致します。
期待通りです。

同様のcodeをnodeコマンドで実行してみましょう。

// Symbol.toPrimitiveの存在確認
console.log(
  "Symbol.toPrimitive exists on Date.prototype:",
  Symbol.toPrimitive in Date.prototype,
); // Symbol.toPrimitive exists on Date.prototype: true

// 実際の変換を試してみる
console.log("Direct console.log:", date); // Direct console.log: 2024-12-28T04:27:35.291Z

// 各hintでの動作確認
console.log('hint "string":', date[Symbol.toPrimitive]("string")); // hint "string": Sat Dec 28 2024 13:27:35 GMT+0900 (Japan Standard Time)
console.log('hint "number":', date[Symbol.toPrimitive]("number")); // hint "number": 1735360055291
console.log('hint "default":', date[Symbol.toPrimitive]("default")); // hint "default": Sat Dec 28 2024 13:27:35 GMT+0900 (Japan Standard Time)

先ほどとは異なる結果になりました。toPrimitiveは存在しているのに、実際のconsole.logの結果はどのhintとも異なるものになっています。

Dateオブジェクトがどのmethodで処理されるかを整理します。

method Chrome Node.js
console.log toString toISOString
Symbol.toPrimitive("default") toString toString

Node.jsでconsole.logを実行するときだけ、なぜかDate.toISOStringが実行されているということがわかります。

なぜDate.toISOString()が実行されている?

Node.jsの実装を見てみましょう。
console.logでは内部的にformatを行っています。
https://github.com/nodejs/node/blob/98d4ebc6d425f55d22b8ab745031cd19f89fd283/lib/internal/console/constructor.js#L323C3-L331

format処理では内部的にutil.inspect()と同様にvalidなDateオブジェクトに対して、明示的にISOStringを呼んでいます。
https://github.com/nodejs/node/blob/ba5992831b4175c086580cfd2adbf07411821669/lib/internal/util/inspect.js#L1016C7-L1026C8

util.inspect()の実装ではこのようになっています。

else if (isDate(value)) {
      // Make dates with properties first say the date
      base = NumberIsNaN(DatePrototypeGetTime(value)) ?
        DatePrototypeToString(value) :
        DatePrototypeToISOString(value);
      const prefix = getPrefix(constructor, tag, 'Date');
      if (prefix !== 'Date ')
        base = `${prefix}${base}`;
      if (keys.length === 0 && protoProps === undefined) {
        return ctx.stylize(base, 'date');
      }
    } else

結論

環境による違いの理解

  • console APIはECMA Scriptの仕様外でありお、実装依存である
  • Nod.jseのconsole.logではdateオブジェクトを受け取ると、明示的にDate.toISOString()を呼び出す
  • Chromeや文字列結合(date + "")の場合は、暗黙的にDate.toString()が呼び出される

おわりに

consoleとDateオブジェクトについて、意外と奥が深いことがわかりました。
フォーマットに依存するパースなどを行う場合は、プラットフォームの違いにより処理が変わることも考慮して、明示的にフォーマットを指定するなどが有益なので、頭の片隅にあると良いかもしれません。

NCDCエンジニアブログ

Discussion