JavaScript におけるthis
がコンテキストによって参照するものが異なるということはこの言語のハマり所としてよく知られていると思います。
今回は Node.js で クラスに定義したプロトタイプメソッド1 を同クラスの別のメソッドでコールバックとして実行した際に、コールバック内の this の挙動が想定通りの挙動にならず悩まされたので対処方法と原因を調査しました。
もし誤りがあればコメント欄などでご指摘いただければ幸いです。
前提
クラス記法を使用するため、バージョンは ES6 を想定しています。
例
以下のSample
クラスを例とします。
class Sample {
constructor(prop) {
this.prop = prop;
}
/**
* `prop`の値をコンソールに出力する
*/
printProp() {
console.log(this.prop);
}
/**
* `printProp`をコールバックとして実行する
*/
printPropAsCallback() {
this.executeCallback(this.printProp);
}
/**
* 引数で渡されたコールバック関数を実行する
*/
executeCallback(callback) {
callback();
}
}
const sample = new Sample("Sample Class");
sample.printProp(); // => ”Sample Class”と表示される
sample.printPropAsCallback(); // => TypeError: Cannot read property 'prop' of undefined
Sample
クラスのインスタンスを生成しprintProp
を実行するとコンソールにprop
の値であるSample Class
が表示されます。
一方、printPropAsCallback
を実行した場合、printProp
がexecuteCallback
のコールバックとして実行されて上記と同じくSample Class
と表示されることを期待しますが、エラーとなってしまいます。
解決方法
解決方法としては以下の 2 つがあります。
- コールバックとして引数に渡す関数内で参照される
this
をbind
メソッドで指定する -
call
またはapply
メソッドを使用して、コールバックとして渡された関数のthis
を指定して実行する -
(3/13 追記)
printProp
をアロー関数でラップしてコールバックとして渡す。
1. コールバックとして引数に渡す関数内で参照されるthis
をbind
メソッドで指定する
1 つ目の解決方法はbind()
を使用して this を指定した上で引数に渡す方法です。
bind
メソッドは、任意の関数につなげて実行すると、bind
の第一引数で指定したオブジェクトをthis
として参照する関数を新たに生成します。
class Sample {
constructor(prop) {
this.prop = prop;
}
/**
* `prop`の値をコンソールに出力する
*/
printProp() {
console.log(this.prop);
}
/**
* `printProp`をコールバックとして実行する
*/
printPropAsCallback() {
// this.printProp を this.printProp.bind(this) に変更
this.executeCallback(this.printProp.bind(this));
}
/**
* 引数で渡されたコールバック関数を実行する
*/
executeCallback(callback) {
callback();
}
}
const sample = new Sample("Sample Class");
sample.printProp(); // => ”Sample Class”と表示される
sample.printPropAsCallback(); // => ”Sample Class”と表示される
上記の例ではbind
メソッドの第一引数にSample
クラスのインスタンスを指すthis
を渡しています。
これによってexecuteCallback
内でprintProp
がコールバックとして実行された際、printProp
内部ではthis
はSample
クラスを指すようになります。
2. call
またはapply
メソッドを使用して、コールバックとして渡された関数のthis
を指定して実行する
2 つ目の解決方法は、call()
またはapply()
により関数内の this の参照を指定した上でコールバックを実行する方法です。
call
およびapply
メソッドは任意の関数につなげて実行すると、call
/apply
の第一引数で指定したオブジェクトをthis
の参照として関数を実行します2。
class Sample {
constructor(prop) {
this.prop = prop;
}
/**
* `prop`の値をコンソールに出力する
*/
printProp() {
console.log(this.prop);
}
/**
* `printProp`をコールバックとして実行する
*/
printPropAsCallback() {
this.executeCallback(this.printProp);
}
/**
* 引数で渡されたコールバック関数を実行する
*/
executeCallback(callback) {
// callback(); を callback.call(this); に変更
callback.call(this);
}
}
const sample = new Sample("Sample Class");
sample.printProp(); // => ”Sample Class”と表示される
sample.printPropAsCallback(); // => ”Sample Class”と表示される
上記例ではcall
メソッドの第一引数に、bind
の時と同じくthis
を渡しています。
これによってexecuteCallback
関数に渡された関数の内部で this はSample
クラスを参照するよう指定されているので、printPropAsCallback
実行時にprintProp
内でSample
クラスのプロパティであるprop
の値を取得して出力します。
(3/13 追記) printProp
をアロー関数でラップしてコールバックとして渡す。
@htsign さんのコメントにある通り、アロー関数にラップして渡す方法でもthis
がクラスを参照するようにすることが可能です。
class Sample {
constructor(prop) {
this.prop = prop;
}
/**
* `prop`の値をコンソールに出力する
*/
printProp() {
console.log(this.prop);
}
/**
* `printProp`をコールバックとして実行する
*/
printPropAsCallback() {
// this.printProp を () => this.printProp() に変更
this.executeCallback(() => this.printProp());
}
/**
* 引数で渡されたコールバック関数を実行する
*/
executeCallback(callback) {
callback();
}
}
const sample = new Sample("Sample Class");
sample.printProp(); // => ”Sample Class”と表示される
sample.printPropAsCallback(); // => ”Sample Class”と表示される
そもそもなぜこの事象が発生するのか
プロトタイプメソッドを直接実行した際にメソッド内のthis
がクラスを参照するため、コールバックとして実行した際にも同じようにthis
がクラスを参照してくれるように思えますが、実際の挙動は両者で異なりました。
原因としては以下の要素が関わっています。
- 関数の呼び出し方による関数内の
this
の参照 - strict モードと非 strict モードにおける関数内の
this
の参照
1. 関数の呼び出し方による関数内のthis
の参照
JavaScript では、関数呼び出し方によってその関数内のthis
の参照先が決定されます。
以下 MDN: this - オブジェクトのメソッドとして より引用
関数がオブジェクトのメソッドとして呼び出されるとき、その this にはメソッドが呼び出されたオブジェクトが設定されます。
次の例では、o.f() が起動したとき、関数内の this には、o オブジェクトが関連付けられます。var o = { prop: 37, f: function() { return this.prop; } }; console.log(o.f()); // logs 37
本記事の例でいうと、sample.printProp();
のように、sample
のメソッドとしてprintProp
を直接呼び出した場合は、printProp
内のthis
は、このメソッドが呼び出されたオブジェクトであるsample
を指します。
そのため、printProp
はsample
のプロパティであるprop
を参照し、その値をコンソールに出力します。
2. strict モードと非 strict モードにおける関数内のthis
の参照
一方、通常の関数呼び出し時にはthis
の値がセットされません。
その場合には、非 strict モードの場合はグローバルオブジェクト(ブラウザではwindow
, Node.js ではglobal
)、strict モードの場合はundefined
がthis
として設定されます。(以下 MDN: this - 単純な呼び出し より)
function f1() { return this; } // ブラウザー上で f1() === window; // true // Node 上で f1() === global; // true
function f2() { "use strict"; // strict モードを見てください。 return this; } f2() === undefined; // true
ここで、クラス内部のコードは常に Strict モードで実行されるという仕様があります。(以下MDN: クラス - プロトタイプと静的メソッドによるボクシングより)
this に値が付けられずに静的メソッドまたはプロトタイプメソッドが呼ばれると、this の値はメソッド内で undefined になります。たとえ "use strict" ディレクティブがなくても同じふるまいになります。なぜなら、class 本体の中のコードは常に Strict モードで実行されるからです。
本記事の初めの例では、executeCallback
は引数に渡された関数callback
を実行していますが、
callback
という値はexecuteCallback
から見たらただの関数なので、たとえクラス内のメソッドを引数として渡したとしても、渡した関数内のthis
の参照は保持されません。
また、クラス内部のコードに常に strict モードが適用されることから、callback
実行時にthis
の参照はundefined
となります。
つまり、printPropAsCallback
実行時にexecuteCallback
内部でコールバックとしてprintProp
が実行された際には、printProp
内部のthis
はundefined
となっているため、undefined
のプロパティを参照しようとしてエラーが起きたということになります。
また、クラスをprototype
で定義した場合、非 strict モードではprintProp
内部のthis
はグローバルオブジェクトになります。
// Node.jsで実行
function Sample(prop) {
// コンストラクタ
// 引数で渡された値をpropに設定する
this.prop = prop;
}
Sample.prototype.printProp = function() {
console.log(this.prop);
};
Sample.prototype.executeCallback = function(callback) {
callback();
};
Sample.prototype.printPropAsCallback = function() {
this.executeCallback(this.printProp);
};
const sample = new Sample("Sample Class");
sample.printProp(); // => ”Sample Class”と表示される
global.prop = "Global Object"; // グローバルオブジェクトにpropプロパティを定義
sample.printPropAsCallback(); // => "Global Object"と表示される
まとめ
- 関数内の
this
の参照は実行時のコンテクストによって決定する- 実行される関数がオブジェクトのメソッドである場合、実行されたオブジェクトが
this
になる - そうでない場合には、strict モードと非 strict モードで動作が異なる。
- 非 strict モードではグローバルオブジェクト (ブラウザでは
window
, Node.js ではglobal
) - strict モードでは
undefined
- 非 strict モードではグローバルオブジェクト (ブラウザでは
- 実行される関数がオブジェクトのメソッドである場合、実行されたオブジェクトが
- プロトタイプメソッドをコールバックとして実行する際には
this
の参照は保持されない - そのため以下の方法で関数内の
this
の値を明示的に指定する必要がある- コールバックとして渡す関数の
this
をbind
メソッドで指定する -
call
,apply
を使用して関数内のthis
を指定した上でコールバックを実行する
- コールバックとして渡す関数の
思ったより非常に高度な内容だったので、繰り返しにはなりますが、誤りがありましたらご指摘いただければ幸いです。
長くなりましたが、ここまで読んでくださりありがとうございました。