本記事では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)としたとき
これは「メソッドとして呼び出した時」に該当します。そのため、this
はconsole
となります。先程のコードを読みやすいように書き換えるとこうなります。
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._ignoreErrors
やglobal._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となります