JavaScript の this キーワードは何を指すのか - コールバック関数内では this を使ってはいけない
this
キーワードの値の決定され方について説明していますが、より全般的な this
キーワードの決まり方について別の記事を書きました。 合わせて参照してください: JavaScript の this キーワードに結びつけられる値はどのように決定されるのか (言語仕様の説明) - ひだまりソケットは壊れないJavaScript における this
キーワードは、簡単なように見えて、その実 JavaScript 初心者にとって落とし穴になりやすいものです。 探せば this
キーワードに関する解説はいくらでもありますが、基本に戻って ECMA-262 *1 を参照しながら解説してみたいと思います *2。
this
キーワードの落とし穴 - コールバック関数がうまく動かない
this
キーワードがどのように落とし穴になりやすいのか考えてみます。 まず、普通に this
キーワードを使ってプログラムを書いてみます。
var obj = {};
obj.message = "メッセージ!!";
obj.callback = function() {
alert( this.message );
};
obj.callback(); // "メッセージ!!" と書かれたアラートが表示される
これを実行すると、コメント中に書かれているように "メッセージ!!" と書かれたアラートが表示されます。 obj.callback
の関数中の this
が obj
を指しており、this.message
が obj.message
と同一であるためです。 これは誰もが納得するでしょう。 問題は、obj.callback
をコールバック関数として setTimeout
や addEventListener
に渡したときに発生します。
// obj は上のサンプルと同じ
setTimeout( obj.callback, 1000 );
// 1000ms 後に "メッセージ!!" と書かれたアラートが表示され――ない!!
これを実行すると、約 1000ms 後に "メッセージ!!" と書かれたアラートが表示される、と思ってしまうかもしれませんが、実際には "undefined" と書かれたアラートが表示されてしまいます *3。
同じ関数を呼び出しているはずなのに、obj.callback()
の形式で呼び出すと期待通りの動作をして、コールバック関数として呼び出すと期待通りの動作をしない。 これが this
キーワードの落とし穴です。
というわけで this
キーワードについて ECMA-262 (Edition 5) で調べてみる
そこで、this
キーワードが何を指すかがどのように決定されるのかを、ECMA-262 (Edition 5) を参照しながら説明します。
JavaScript や JScript の標準として ECMAScript というものがあり、その公式文書が ECMA-262 です。 ここでは Edition 5 (PDF) を参照しながら説明したいと思います。
関数呼び出し
関数呼び出しについては、11.4.3 節に述べられています。 関数呼び出しの形式は MemberExpression Arguments というもので、MemberExpression は変数だったり関数式だったりします。 Arguments は括弧でくくられた引数のリストです。 関数呼び出し時の手順 (11.4.3 節に書かれている内容を訳したもの) を以下に示します。
- 1. MemberExpression の実行結果を ref とする
- 2. GetValue(ref) の結果を func とする
- 3. Arguments の実行結果を argList とする
- 4. Type(func) が Object でなければ TypeError 例外を発生させる
- 5. IsCallable(func) が false なら TypeError 例外を発生させる
- 6. Type(ref) が Reference の場合:
- a. IsPropertyReference(ref) が真の場合:
- i. GetBase(ref) の結果を thisValue とする
- b. そうでない場合 (ref の base は Environment Record):
- i. GetBase(ref) のメソッド ImplicitThisValue を呼び出した結果を thisValue
- 7. そうでない場合 (Type(ref) が Reference でない):
- a. thisValue は undefined
- 8. this の値として thisValue を、引数リストとして argList を提供して func の内部メソッド [[Call]] を呼び出し、その結果を返す
よくわかんないかも知れませんが、
obj.function_name()
というように、obj のプロパティとして関数を参照して関数呼び出しを行った場合 (6.a の場合)、またはwith( obj ) {
function_name(); // function_name は obj のプロパティ
}
という形で with
文を使って関数を参照して関数呼び出しを行った場合 (6.b の特殊な場合)、thisValue は obj
となります。 一方でvar function = function() { ... };
function();
のように、関数を参照している局所変数を使って関数呼び出しを行った場合 (6.b の場合)、または(function() { ... })();
というように関数式で作成した関数をそのまま呼び出すような場合 (7 の場合)、thisValue は undefined
となります。thisValue を決定した後、内部関数 [[Call]] が呼び出されます。
内部関数 [[Call]] の呼び出し
内部関数 [[Call]] については、13.2.1 節に書かれています。 this
の値は 10.4.3 節で説明されていると書かれています。
this
の値の決定
というわけで 10.4.3 節を見てみます。 10.4.3 節は、コードの実行ステップが関数に入るときの処理について書かれており、this
の値についても述べられています。 this
に関する部分は、以下のとおりです。
- 1. If the function code is strict code, set the ThisBinding to thisArg.
- 2. Else if thisArg is null or undefined, set the ThisBinding to the global object.
- 3. Else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).
- 4. Else set the ThisBinding to thisArg.
thisArg というのが、thisValue のことを意味しており、ThisBinding というのが this
キーワードに結び付けられるものを表しています。
まず、strict モードの場合、this
は thisArg そのままになります。 thisArg が undefined
や null
でもそのままです。
ただ、strict モードで実行することはあまりないと思います。 strict モードでなく、thisArg が null
または undefined
の場合、this
はグローバルオブジェクトを指すことになります。 それ以外の場合、thisArg そのもの (プリミティブ型ならばそれをオブジェクト化したもの) を指すことになります。
this
キーワードの決定方法についてのまとめ
重要なのは this
キーワードは関数がどのようにして呼び出されたかによって決まる ということです。 基本的には、
- オブジェクト (またはプリミティブ型) のプロパティから参照されている関数を呼び出した場合は
this
はそのオブジェクトを指す - それ以外の場合、
this
はグローバルオブジェクト (strict モードでない場合)
です。
そんなわけでコールバック関数内では this
を使わないようにすべし
さて、ここまでくるとなぜコールバック関数として渡した関数の中の this
は期待通りのオブジェクトを指さないのかがわかったと思います。 なんらかのプロパティから参照されている関数を別の関数に渡したとき、渡した先の関数内ではもはやそれはただの局所変数から参照されているだけの関数になってしまうからです。
function do_func( func ) {
func(); // 渡された関数 func は, もはや局所変数から参照されているだけ
}
do_func( obj.func ); // obj のプロパティ func から参照されている関数を渡しても
// それは obj のプロパティから参照されていることは
// 渡した先の関数ではわからない
この問題への対処法はまあ色々とあるかと思いますが、一番簡単でわかりやすいのは コールバック関数として別の関数に渡す関数の中では this
を使わない というものだと思います。 理解してしまえばなんてことはないですが、JavaScript を使い始めた最初のうちはついついこの落とし穴にはまってしまうことが多いと思いますので気をつけてください。
補足
この記事ではコールバック関数の中における this
キーワードについて注目しましたが、コールバック関数として別の関数に渡したときだけでなく、別のオブジェクトのプロパティに代入したときにも this
の指すものは変わってきます。
var obj1 = { message: "this is obj1!!" };
var obj2 = {};
obj1.show_msg = function() { alert( this.message ); };
obj2.show_msg = obj1.show_msgobj1.show_msg(); // "this is obj1!!" というアラート
obj2.show_msg(); // "undefined" というアラート
ただ、こっちはそれなりにわかりやすいので悩むことはあんまりないと思います。 コールバック関数として別の関数に渡したときに悩む人は多い気がしますので、この記事ではコールバック関数中の this
キーワードに注目しました。