【Chrome】参照型変数を console.log/dir した時の挙動が怪しい

2017年04月19日

カテゴリー:

私はWEB制作ではChromeをメインブラウザとして使っています。JavaScriptをテストする時もChromeのデベロッパーツールを重宝しているのですが、なんとなく、オブジェクトや配列のような参照型変数を console.logconsole.dir で出力した時の挙動が怪しい(クセがありそうな)気がしたので少し調べてみました。

どの辺が怪しいかを簡潔に説明するために、単純化したコードを次に示します。

'use strict';

var obj = {message: ''};

var fn1 = function () {
  obj.message = 'fn1 called!';
  console.dir(obj); // (A)
};

var fn2 = function () {
  obj.message = 'fn2 called!';
  console.dir(obj); // (B)
};

fn1();
fn2();

まずは dir メソッドでオブジェクトを出力してみます。
上記コード例の場合、(A)と(B)の出力内容はどのようになるでしょうか?

(A) の方は { message: ‘fn1 called!’ }
(B) の方は { message: ‘fn2 called!’ }

が出力されることを期待したコードでしたが、結果は次の通り。

(A) の時点では message プロパティの値は「fn1 called!」です。にもかかわらず、なぜかどちらの message も「fn2 called!」が表示されています。

では次に、先ほどのコードの console.dir を console.log に置き換えるとどうなるでしょうか?

今度は期待する結果が得られた、、、と思いきや、ログの左側にある三角マークをクリックして中身を展開すると、

console.dir を使った場合同様、オブジェクトを展開した後の内容はどちらの message プロパティも「fn2 called!」になってしまいます。

ならば、さらに console 部分を少し変えて、次の様に直接プロパティを出力させるとどうなるのか?

'use strict';

var obj = {mesage: ''};

var fn1 = function () {
  obj.message = 'fn1 called!';
  console.dir(obj.message); // (A)
};

var fn2 = function () {
  obj.message = 'fn2 called!';
  console.dir(obj.message); // (B)
};

fn1();
fn2();

まぁ当然と言えば当然ですが、こちらは期待した通りの出力でした。
どうやら、参照型変数をそのまま console.log/dir で出力させると、先ほどのようなおかしな挙動を示す、そんな気がします。

ここまではオブジェクトを例にしましたが、配列も参照型なので次のコードでテストしてみます。

'use strict';

var arr = ['hoge'];

var fn1 = function () {
  console.dir(arr); // (A)
};

var fn2 = function () {
  arr[0] = 'fuga';
  console.dir(arr); // (B)
};

fn1();
fn2();

(A) の時点での変数 arr の中身は [‘hoge’] なのですが、出力内容を展開すると [‘fuga’] になっています。やはり、オブジェクトの場合と同様の挙動を示すようです。

色々試してみましたが、console.log/dir を使って参照型変数を出力する場合、Chromeのデベロッパーツール上でその変数を展開すると、"展開したタイミング(三角マークをクリックしたタイミング)"での変数の内容が表示されるみたいですね。

このことを次のコードで確認してみます。

var obj = {name: 'taro'};

console.log(obj);
console.log(obj);

これをデベロッパーツールのこコンソールに出力させると

まずは上の様に表示されます。この状態で1つ目の出力を展開してみます。

このタイミングでは出力対象のオブジェクトには何も変更が行われていないので、上図のような表示になります。
ここでさらに、コンソール上で直接 obj.name の内容を更新してみます。

obj.name プロパティの更新が終わったら、未展開のログ(2つ目のObject)を展開してみます。

展開されたオブジェクトの name プロパティの値が hanako になっていることが確認できます。


Chromeデベロッパーツールでは console.log/dir で参照型変数(オブジェクト、配列)を出力した場合、ログに表示された変数を展開すると、展開された内容は、「展開したタイミング」の内容が表示されます。

言い換えると、log/dir メソッドで出力した後に、そのオブジェクト/配列に何らかの変更が加えられている場合、展開前と展開後の内容は違うものになる、と言えるでしょう。

log メソッドを使った場合、展開前は出力時の変数の内容が表示されています。通常はこれで事足りるのであまり問題になることはありませんが、出力対象のオブジェクトが多くのプロパティを持っていたりすると、展開前の表示だけではオブジェクトの内容が把握しにくくなります。ですが、オブジェクトの内容を展開すると、その内容は展開されたタイミングの内容になるので注意が必要です。

dir の場合、展開しないと内容が確認できない、つまり展開したタイミングでの変数の状態しか確認できません。以前は log メソッドでオブジェクトのプロパティまで確認することができなかったので、dir メソッドは便利な物でしたが、最近のChromeでは log メソッドでもプロパティの内容を表示できるので、個人的には dir メソッドに必要性を感じません。

その時点でのオブジェクトの中身を表示する方法

ここまでの内容を踏まえると、多くのプロパティを持つオブジェクトの出力時の状態を確認するには log/dir メソッドだけでは困難だと言えそうです。

console オブジェクトには log/dir 以外にも様々なメソッドが用意されていますが、私が確認した限りでは、「出力時のオブジェクトの状態」をちゃんと表示してくれるものはありませんでした。

そこで、(ニーズがあるかどうかは別として)そういった出力を得るための、私なりの解決策を2つ考えてみました。

1.黒魔術的解決策

'use strict';

var obj = {name: 'taro'};

console.log(JSON.parse(JSON.stringify(obj)));
console.log(JSON.parse(JSON.stringify(obj)));

出力時点でのオブジェクトを一旦テキストにシリアライズして、さらにその文字列をデシリアライズすることで、オブジェクトのコピーを作成するというもの。とりあえず最初に思いついたのがこの方法だったので書いてみましたが、他にもっとスマートな方法ありそうな気がします。

2.別のブラウザを使う

本稿ではChromeでの挙動をテーマにしていますが、コンソールへの出力に関してはブラウザ依存の部分が大きいので、他のブラウザを使うという手もあります。実際、各ブラウザ毎に実装にバラつきがあります。

例えば、Edge の console.log であれば、参照型を出力しても出力時点での内容が展開されるようです。
下図は本稿冒頭のコードをEdgeのデベロッパーツールで見たものですが、出力の展開前と展開後とでプロパティの内容が一致していることが確認できます。

まとめ

今回のテーマはJavaScriptというよりはブラウザのデベロッパーツールの実装に依存する内容です。また、ブラウザのバージョンアップによりこのあたりの仕様も変更される可能性もあります。更に言うと、今回説明したようなことで躓くケースというのも稀な気がします。つまり、かなりニッチな内容です。

ですので、この辺の挙動や仕様について深く掘り下げて学ぶ必要は全然ないと思いますが、もし console.log/dir の結果が意図したものにならない時、デバッグ時の余計な混乱を避ける意味では今回紹介した内容を頭の片隅に入れておいて損はないと思います。

※Chromeデベロッパーツールの場合・・・

  • console.log/dir で参照型変数を出力した後に、変数の内容(プロパティ、配列の要素)が変更された場合、コンソール上で 「出力を展開」 した時に得られる内容は変更後(出力を展開したタイミング)の内容になる。
  • console.dir メソッドは出力内容を展開しないと中身が確認できない。つまり、展開時の内容しか確認できないので、信頼度が低い
  • console.log メソッドで、出力時の内容を確認したい場合は、工夫する必要がある。(出力コードのカスタマイズ、ブラウザを変える など)