Blog, The

1 月 22 日生まれ、ブログ座です。

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 は長いことやってても、未だにこういう発見があったりして面白いですね。