IE の console.log がよくわからない
きっかけ
デバッグ用に console をラップしたオブジェクトを作ってて、Sinon.js を使ってテストをしてたら、なんか IE でエラーが出る。
調べてみると Sinon.js 内にこんなコードがあって・・・
function isFunction(obj) { return !!(obj && obj.constructor && obj.call && obj.apply); }
これが console.log に対して false を返してるのが原因らしい。
true を返しそうなもんだけど・・・
どうなってんの?
どうも納得がいかなかったので、IE8 と IE9 のコンソールで色々試してみた。
- typeof console.log は "object" を返す
- Object.prototype.toString.call(console.log) は "[object Object]" を返す
- 関数じゃないらしいので、apply や call は未定義
- でも、コンソールで console.log を評価すると "function log() { [native code] }" って言ってる
- ていうか、 toString や hasOwnProperty なんかも未定義
- console.log.foo = "bar" としたら "オブジェクトでサポートされていないプロパティまたはメソッドです。" って怒られた
なるほど。詳しいことはよく分からんが特別な存在と言うことですね。
なんというか、自分が JavaScript という中で積み上げてきたものを、ことごとく打ち砕かれた。そんな思いです。
どうしよう?
冒頭のコードで false を返してた理由はわかりましたが、どうしたものか・・・
true を返すようにプロパティを追加するという、場当たり的な対応もできませんし、テスト対象のコードも console.log.apply(console, [...]) てな感じなので、このままでは動きません。
なんとかしたい。
調べてたら、なんとかなりそうなのが見つかりました。
javascript - console.log.apply not working in IE9 - Stack Overflow
一番 vote されてる回答の中で、解決策らしきコードが示されてます。
if (Function.prototype.bind && console && typeof console.log == "object") { [ "log","info","warn","error","assert","dir","clear","profile","profileEnd" ].forEach(function (method) { console[method] = this.bind(console[method], console); }, Function.prototype.call); }
console の各種メソッドを、「Function#call に元のメソッドをバインドし、第一引数に console を適用した Function オブジェクト」に置き換えています。
書いてて意味が分からないんですが、console.log("foo", "bar", "baz") としたときに、こんなコードが実行されるイメージでしょうか。
Function.prototype.call.apply(console.log, [console, "foo", "bar", "baz"]);
特別な存在である console.log ですが、Function.prototype のメソッドは使えるんですね。
内部的に関数として呼び出せるインターフェースは持ってるってことですかね?
なにはともあれ、これで結果は変えずに、console のメソッドを真の Function オブジェクトに置き換えることができそうです。
IE9 ではこのままで問題無いんですが、Function#bind, Array#forEach は IE8 じゃ使えないので、自分なりにアレンジして、最終的にはこんな感じになりました。
if (typeof console !== "undefined" && typeof console.log === "object") { (function() { var slice = Array.prototype.slice; var bind = Function.prototype.bind || function(context) { var args = slice.call(arguments, 1); var self = this; return function() { return self.apply(context, args.concat(slice.call(arguments))); }; }; var methods = ["log", "info", "warn", "error", "assert", "dir", "clear", "profile", "profileEnd"]; for (var i = 0, length = methods.length; i < length; i++) { console[methods[i]] = bind.call(Function.prototype.call, console[methods[i]], console); } })(); }
コンソールでざっと試した感じだと動いてるっぽい。
JavaScript は長いことやってても、未だにこういう発見があったりして面白いですね。