LoginSignup
101
61

console.log の落とし穴:console.log にはオブジェクトの参照が渡るので気を付けよう

Last updated at Posted at 2019-05-20

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

console_log_chrome.png

FireFox66

console_lof_FF.png

ちなみにChromeでは、横のiアイコンをホバーすると「値はたったいま評価されたものだよ」と教えてくれます。

chrome_tip.png

なぜ呼び出した時点の値を教えてくれないのか

このStack Overflowでは、

  • 各時点でのオブジェクトを保持しておくとすると、メモリや計算量的にヤバいから
  • 参照ではなく文字列として保持しておくとすると、循環参照があった場合にマズいから

などの理由が指摘されていました。

ではどうすればいいか

① コピーを渡す
let obj = {
  a: 'foo'
}

console.dir({...obj}) // => {a: 'foo'}
obj.a = "bar"
console.dir(obj) // => {a: 'bar'}

スプレッド構文で(シャロー)コピーを作って渡しているため、obj.aは後の変更の影響を受けません。

solution.png

ただしコピーは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()で大きなオブジェクトや配列のデバッグをするときは、参照が渡る挙動をすることを忘れないようにしましょう(私は忘れて混乱することがありました)。

101
61
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
101
61