TL; DR
console.log()
に大きなオブジェクトや配列を渡すと、console.log()
を呼び出した時点での値ではなく、コンソールでそれをクリック展開した時点で評価した値が表示される。
もう少し詳しく
console.log()
に大きなオブジェクトを渡すとコンソール上で省略されて表示されますが、その省略表示を展開すると、console.log()
が呼び出された時点の値ではなく、展開したその時点で評価した値が表示されます。
つまり例えばあるオブジェクトに操作を施す前にconsole.log()
を取っていても、実行終了後のコンソールには操作を施した後のオブジェクトが表示されるということです。
要はオブジェクトへの参照を保持しているような挙動です(参考)。
例
(以下の例ではわかりやすさのため常に省略表示されるconsole.dir
を使っています)
obj.a
をに値を代入する前後でログを取っているのに、代入後の値しか取れていません。
let obj = {
a: 'foo'
}
console.dir(obj) // => {a: 'bar'}
obj.a = "bar"
console.dir(obj) // => {a: 'bar'}
確認した限りではChrome, Firefox, Edge, IE11の2019年5月現在の最新バージョンで再現できました。
Chrome74
FireFox66
ちなみにChromeでは、横のi
アイコンをホバーすると「値はたったいま評価されたものだよ」と教えてくれます。
なぜ呼び出した時点の値を教えてくれないのか
- 各時点でのオブジェクトを保持しておくとすると、メモリや計算量的にヤバいから
- 参照ではなく文字列として保持しておくとすると、循環参照があった場合にマズいから
などの理由が指摘されていました。
ではどうすればいいか
① コピーを渡す
let obj = {
a: 'foo'
}
console.dir({...obj}) // => {a: 'foo'}
obj.a = "bar"
console.dir(obj) // => {a: 'bar'}
スプレッド構文で(シャロー)コピーを作って渡しているため、obj.a
は後の変更の影響を受けません。
ただしコピーは1段階の深さで行われるため、obj
がネストしている場合ネストされた部分は変更の影響を受けてしまいます。
その場合は何らかのライブラリのディープコピー用関数を用いるしかなさそうです。
② 一度文字列に変換する
MDNにあった解決法です。
let obj = {
a: 'foo'
}
console.log(JSON.parse(JSON.stringify(obj)));
obj.a = "bar"
console.log(JSON.parse(JSON.stringify(obj)));
ただし文字列にする関係上、循環参照があった場合はエラーになります。
③ ブレークポイントを使う
欲しい所で止めれば当然その時点の値が取れます。
④ プロパティの値を渡す
追跡したいプロパティa
の値がプリミティブ型ならconsole.log(obj.a)
のようにやるのが簡単ですね。
⑤ オブジェクトを変化させない
オブジェクトがイミュータブルであればいつconsole.log
を取っても同じ値が表示されます!
他にも良い方法があれば是非教えてください。
まとめ
console.log()
で大きなオブジェクトや配列のデバッグをするときは、参照が渡る挙動をすることを忘れないようにしましょう(私は忘れて混乱することがありました)。