Panda Noir

JavaScript の限界を究めるブログでした。最近は幅広めに書いてます。

log = console.log;はなぜダメなのか

本記事ではNodeJSのコードを検証しています。ChromeやSafariなどブラウザによっては実装が異なる可能性があります。

consoleのソースコードを読む

GitHub上に上がっています。node/lib/console.js

ここの以下のコードがポイントとなります。

Console.prototype.log = function log(...args) {
  write(this._ignoreErrors,
        this._stdout,
        // The performance of .apply and the spread operator seems on par in V8
        // 6.3 but the spread operator, unlike .apply(), pushes the elements
        // onto the stack. That is, it makes stack overflows more likely.
        util.format.apply(null, args),
        this._stdoutErrorHandler,
        this[kGroupIndent]);
};

consoleではなくConsole.prototypeとなっていますが、console = new Console(process.stdout, process.stderr)なのでそこまで気にしなくて大丈夫です。

thisは呼び出し方によって変わる

「JavaScriptのthisは関数の呼び出し方によって変わる」、ということはQiitaですでに100万回は書かれているので、詳細は省きます。詳しく知りたい方はJavaScriptの「this」は「4種類」??を参照ください。

ここでポイントは

  • 関数として呼び出した時はthisがグローバルオブジェクト(NodeJSの場合はglobal)になる*1
  • メソッドとして呼び出した時はthisがそのインスタンス自身になる

この2点です。

console.log(...args)としたとき

これは「メソッドとして呼び出した時」に該当します。そのため、thisconsoleとなります。先程のコードを読みやすいように書き換えるとこうなります。

Console.prototype.log = function log(...args) {
  write(console._ignoreErrors,
        console._stdout,
        // The performance of .apply and the spread operator seems on par in V8
        // 6.3 but the spread operator, unlike .apply(), pushes the elements
        // onto the stack. That is, it makes stack overflows more likely.
        util.format.apply(null, args),
        console._stdoutErrorHandler,
        console[kGroupIndent]);
};

log(..args)とした場合

log = console.logと代入してlog(...args)と呼び出した場合はthisはグローバルオブジェクト(global)となります。そのため先程のコードは次のようになります。

Console.prototype.log = function log(...args) {
  write(global._ignoreErrors,
        global._stdout,
        // The performance of .apply and the spread operator seems on par in V8
        // 6.3 but the spread operator, unlike .apply(), pushes the elements
        // onto the stack. That is, it makes stack overflows more likely.
        util.format.apply(null, args),
        global._stdoutErrorHandler,
        global[kGroupIndent]);
};

もうおわかりですね?global._ignoreErrorsglobal._stdoutが存在していないのです。そのため、エラーとなります。

どうすればlog(...args)とできるのか

以下の2つが一般的な方法です。

const log = console.log.bind(console); // thisをconsoleで固定する
const log2 = (...args) => console.log(...args); // console.logを内部で呼び出す

しかし、我々は先程のコードを解析することで新しい手法ができることに気が付きました。第三の方法です。

global._ignoreErrors = console._ignoreErrors;
global._stdout = console._stdout;
global._stdoutErrorHandler = console._stdoutErrorHandler;

これで実行すれば・・・

そもそもlog=console.log;で問題がなくなっていた

よくコードを読んでみたら

function Console(stdout, stderr, ignoreErrors = true) {

  // ...

  // bind the prototype functions to this Console instance
  var keys = Object.keys(Console.prototype);
  for (var v = 0; v < keys.length; v++) {
    var k = keys[v];
    this[k] = this[k].bind(this);
  }
}

というコードを見つけました。console.logはすでに.bind(console)されたあとだったのです。そのため、上の黒魔術じみたことをしなくてもlog = console.log;で動きます。しかもこの変更があったのはv0.10.0以前なので現代のNodeJSなら何も問題なく使えます。

ついでにChrome 63.0.3239.132でも検証したところ、log = console.log;で動作しました。

結論とお詫び

現代のJavaScriptは進化していました。タイトルに「log=console.logはなぜダメなのか」などとつけてしまったことを謹んでお詫び申し上げます。

もちろん、上のようにあらかじめthis[key].bind(this)されていない可能性があるので、安易にmethod = someObj.methodとしてはいけません。注意してください。

*1:注: strictモードを有効にしている場合、thisはグローバルオブジェクトではなくundefinedとなります