QA@IT

JavaScriptの Uncaught TypeError: Illegal invocation はどういう意味?

14685 PV

JavaScriptで printfデバッグのために、毎回 console.log と打つのが嫌になって、次のように pp と名前を付けました。ところが、3行目で Uncaught TypeError: Illegal invocation というエラーが出ます。

var pp = console.log;
var elm = document.getElementById("foo");
pp(elm);  // Uncaught TypeError: Illegal invocation

私の理解では、console.log は Functionオブジェクトへの参照を持っていて、ppにはその参照が入るので呼び出せるということなのですが、なにがイケないのでしょうか? pp.call(elm) としてもうまくいきません。

ブラウザはChomeを使っていて、この機能についてはクラスブラウザとかは気にしていません。

回答

解説は他の方が既にされているので、まだ挙げられていない解決法をひとつ。

ECMAScript5のbindを実装している実行環境なら次のようにもできます。

var log = console.log.bind(console);
log("foo");

console.logが複数の引数を取れるようになっている実行環境なら第一引数を固定(カリー化)しておくこともできるので便利かもしれません。

var module1log = console.log.bind(console, "### module1\n");
module1log("foo");
編集 履歴 (2)
  • この場合はカリー化というか部分適用ですね、すみません。 -
  • なるほど、明示的に関数呼び出し時の this を console に bind するということですね。ありがとうございます。 -

thisの問題です
これはdocument等でも起こります

例えば
document.body.getElementsByTagName()
これはdocument.bodyのgetElementByIdメソッドが呼ばれるわけではありません
document.body→HTMLBodyElement→HTMLElement→Element
のようにprotoを辿ってElement.getElementsByTagNameを参照しています
全ての要素に専用の関数を付けるとコストが莫大になってしまうのでこのように親を辿る設計になっているのです

ですからfunc=document.body.getElementsByTagNameとしたとき、
funcは実際には「Element」というコンストラクタにある関数を指していて、document.bodyと関係を持ちません、funcは自身が何を対象にするかという情報を持っていないのです

そこでどうやって対象を把握するかというと、thisを使うわけです
document.body.getElementsByTagName()のようにすると
Element.getElementsByTagName内部のthisがdocument.bodyを指します
そして対象がdocument.bodyだと把握できる判断わけです

しかしfunc()のようにすると
thisはwindow、またはnullを指し
対象がwindowやnullになるのでエラーになります

これはconsoleでも同じです
console.logもConsole.logから来ています
それは標準出力に引数を書き出すというものです
しかし、標準出力が「ブラウザのコンソール」であるという情報等はconsoleが持っており
Console.logはthis値を使ってそれを参照できないと困ります
それでエラーになるわけです

簡単にまとめると、ネイティブで用意されているこういった関数をむやみに変数に入れるなということです
どうしても入れたい場合はapplyで適切なthisを渡してあげてください
var pp=function(){console.log.apply(console,arguments)}

編集 履歴 (0)
  • なるほど!! です。とても良く分かりました。「対象」というのは、関数評価時のコンテクストとか環境とか、そう呼ばれることもあるようなナニモノかでしょうか。関数型っぽいことの基本が私には分かっていないような気がしてきました。Cの関数ポインタのように何でも持ち運べばいいというような話ではないのですね。この問題については、プロトタイプチェーンのご説明で良く分かりました。ありがとうございます! -

かなり昔に FireFox でそれをやろうとしたときは、console.log が console オブジェクトのメソッドなので出来なかったように思います。

var c = console.log;
c.apply(console, ["hoge"]);

次の方法だと呼び出し位置が全部同じ位置になってしまうので、

var c = function(value){console.log(value)};
c("hoge");

少ないタイプ数でどうにかなるように、苦肉の策で次のようにしました。

var z = console;
z.x = console.log;
z.x("hoge");
編集 履歴 (2)
  • おっと、c.call(console, val) でいいんですね。実は c.call(window, val) として動かないなぁと悩んでいました。z.x だとだいぶタイプ数が少ないですね。ありがとうございます。 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。