JavaScript の this キーワードは何を指すのか - コールバック関数内では this を使ってはいけない

本記事では、関数呼び出し (Function Call) の際の 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 の関数中の thisobj を指しており、this.messageobj.message と同一であるためです。 これは誰もが納得するでしょう。 問題は、obj.callback をコールバック関数として setTimeoutaddEventListener に渡したときに発生します。

// obj は上のサンプルと同じ
setTimeout( obj.callback, 1000 );
// 1000ms 後に "メッセージ!!" と書かれたアラートが表示され――ない!!

これを実行すると、約 1000ms 後に "メッセージ!!" と書かれたアラートが表示される、と思ってしまうかもしれませんが、実際には "undefined" と書かれたアラートが表示されてしまいます *3

同じ関数を呼び出しているはずなのに、obj.callback() の形式で呼び出すと期待通りの動作をして、コールバック関数として呼び出すと期待通りの動作をしない。 これが this キーワードの落とし穴です。

というわけで this キーワードについて ECMA-262 (Edition 5) で調べてみる

そこで、this キーワードが何を指すかがどのように決定されるのかを、ECMA-262 (Edition 5) を参照しながら説明します。

JavaScriptJScript の標準として 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 が undefinednull でもそのままです。

ただ、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_msg

obj1.show_msg(); // "this is obj1!!" というアラート
obj2.show_msg(); // "undefined" というアラート

ただ、こっちはそれなりにわかりやすいので悩むことはあんまりないと思います。 コールバック関数として別の関数に渡したときに悩む人は多い気がしますので、この記事ではコールバック関数中の this キーワードに注目しました。

*1:JavaScript の標準みたいなもの

*2:自分自身の勉強が主な目的だったりするので、もしかしたら間違ってる箇所があるかもしれません。 その場合は指摘をお願いします。

*3:実装依存かも