JavaScript の this キーワードに結びつけられる値はどのように決定されるのか (言語仕様の説明)
最近 JavaScript の this
キーワードについての記事をいくつか見かけて 「そういや自分も昔 this
キーワードについて記事を書いたなー」 と思って 古い記事 を見返してみたのですが、関数呼び出しのことしか説明してなかったので改めて this
キーワードの全般的な話を書いておこうかと思います。 本記事は ECMA-262 5.1th に基づいています。 初心者向けの this
キーワードの使い方の指針を示しているわけでも JavaScript 処理系の実装の説明をしているわけでもなく、JavaScript 言語コアの仕様を説明していることに注意してください。
- ECMA-262 5.1th : JavaScript の言語コア部分 (ECMAScript) の言語仕様 (バージョン 5.1)
初心者向けの this
キーワード周りの指針
この記事の最後の 「まとめ」 に、自分が JS 書くときに this
キーワードではまらないようにするための指針を書いてますのでよければご覧ください。
最近見た記事について
- JavaScript の this を理解する - tacamy memo : Java ユーザーから見た JavaScript の
this
キーワードについて; 混乱しやすい箇所について思考の流れがわかる感じで説明されてるので Java とかから JS に来た人には良さそう; 厳密さに欠けるのはちょっと気になる - JavaScriptのthisの覚え方 #JavaScript - Qiita : わかる人にはわかるのかもしれないけどどこか引っかかる感じがした; 「何かに所属」 ってどういう意味なんだ? と思ったり *1; 「関数呼び出し時に
this
の値が決まる」 というような説明があればもっとわかりやすい気がした - JavaScriptのthis - Write and Run : 言わんとするところはわかるけど、「レシーバ」 という言葉で全部片づけられても困る、って思った; まあ大体こういう理解をしておけば混乱はしないと思う
this
キーワードとは何か
そもそも this
キーワードとは何かという話。
ECMA-262 には、“The
this
keyword evaluates to the value of the ThisBinding
of the current execution context.” (ES-11.1.1) と書かれています *2。 つまり、this
キーワードというのは実行コンテキストに結びついているものなわけです。
では実行コンテキスト (ES-10.3) というのは何なのかという話になるわけですが、要は実行可能コードに関する変数の管理をするものと思えば良いでしょう *3。 例えば、関数内で定義された変数は、その関数コードに結びついている実行コンテキスト内で管理されています。 そして、実行コンテキストによって管理されるもののうちの 1 つに thisBinding
があるわけです。
実行コンテキストが生成されるのは、新しい *4 実行可能コードに制御が移るときです。 そして、そのときに ThisBinding
の値がセットされます (ES-10.4)。
ここで説明したことを簡潔に言うと、『this
キーワードを評価したときの値は、その this
キーワードが使われている実行可能コードに制御が移ったときに決定されるものである』 ということになります。
3 種類の実行可能コードとそれぞれの場合の this
の値の決定方法
ECMAScript においては、実行可能コードには 3 種類あります (ES-10.1)。
- グローバルコード (Global Code)
- Eval コード (Eval Code)
- 関数コード (Function Code)
まあ読んで字のごとくだと思うので説明はしません。 詳細は ECMA-262 を見てください。 ここでは、それぞれの実行可能コードにおける this
キーワードの値 (すなわち ThisBinding
の値) の決められ方について説明します。
グローバルコードにおける ThisBinding
グローバルコードの実行コンテキストに制御が入ったとき (ES-10.4.1) には、ThisBinding
にグローバルオブジェクトがセットされます (ES-10.4.1.1)。
Eval コードにおける ThisBinding
eval
関数の呼び出し側のコンテキスト (calling context) が存在しない場合、またはeval
関数が direct call (ES-15.1.2.1.1) されたわけではない場合は、グローバルコードに制御が移るときと同様に実行コンテキストが初期化されます- つまり、
ThisBinding
の値はグローバルオブジェクトになります - 呼び出し側のコンテキストが存在しない場合というのは、
eval
関数に文字列を引数として渡した場合かと思っていますがよくわかんないです
- つまり、
- それ以外の場合、
ThisBinding
の値は呼び出し側のコンテキストにおけるThisBinding
の値と同じになります
Strict Mode では色々と eval
関数の動作が変わったりしますが、ThisBinding
の値は strict mode でもそうでなくても変化はないと思っています (自信なし)。 詳細は ECMA-262 5.1th の 10.4.2 をご覧ください。
関数コードにおける ThisBinding
最後に、関数コードにおける ThisBinding
の決められ方 (ES-10.4.3) を説明します。
まず基本的なこととして、関数の呼び出し側から与えられる this
値が実行コンテキストの ThisBinding
の値になる ということがいえます (例外あり、後述)。 関数の呼び出し側というのは、厳密に定義すると、呼び出される関数オブジェクトの内部メソッド [[Call]]
(ES-13.2.1) を呼び出しているところです。 (内部メソッド [[Call]]
というのは、言語仕様の説明のために使われているメソッドです。) [[Call]]
は以下のような箇所で呼び出されます。
- 通常の関数呼び出しの式 :
[1,2,3].push(1)
のような式の評価時 Function.prototype.call
メソッドやFunction.prototype.apply
メソッドの中Function.prototype.bind
メソッドにより生成された関数オブジェクトの内部メソッド[[Call]]
(ES-15.3.4.5.1) の中 *5- 内部メソッド
[[Construct]]
(ES-13.2.2) の中 : 内部メソッド[[Construct]]
が呼び出されるのはnew
演算子を用いた式の評価時 (ES-11.2.2)
例外の話
『関数の呼び出し側から与えられる this
値が実行コンテキストの ThisBinding
の値になる』 と言いましたが、正確な仕様は ECMA-262 5.1th の 10.4.3 に書かれています。 ここでいう thisArg
というのは、関数オブジェクトの内部関数 [[Call]]
呼び出し時に渡される this
値のことです。
- 1. 関数コードが strict code であれば、
thisArg
がThisBinding
になる- 2. そうでない場合で、
thisArg
がnull
かundefined
であれば、ThisBinding
にはグローバルオブジェクトがセットされる- 3. そうでない場合で、
Type(thisArg)
がオブジェクトでないならば、ThisBinding
にはToObject(thisArg)
がセットされる- 4. そうでない場合は、
ThisBinding
にはthisArg
がセットされる
つまり、strict mode じゃなくて、this
値として渡された値がオブジェクトじゃなければ、this
値がそのまま this
キーワードの値になるわけではない、ということです。
通常の関数呼び出し (Function Call)
通常の関数呼び出しの式がどのように評価されるかについては、ECMA-262 5.1th の 11.2.3 に書かれています。
関数呼び出しの形式は MemberExpression Arguments
というもので、MemberExpression
が呼び出される関数を表す式 (変数だったり関数式だったり)、Arguments
が括弧でくくられた引数のリストです。 関数呼び出し時の手順 (11.2.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]] を呼び出し、その結果を返す
よくわかんないかも知れませんが、Reference 型とは何か *6、などを詳細に説明しだすと長くなってしまうので、具体例を挙げて説明します。
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
が、this
値として内部関数 [[Call]]
に渡されます。
Function.prototype.call
メソッドや Function.prototype.apply
メソッド
ECMA-262 5.1th の 15.3.4.4 や ECMA-262 5.1th の 15.3.4.3 を見ればわかるように、これらのメソッドに渡された第 1 引数がそのまま this
値として内部関数 [[Call]]
に渡されます。
Function.prototype.bind
メソッドによって生成された関数オブジェクトの内部メソッド [[Call]]
の中
Function.prototype.bind
メソッドにより生成された関数オブジェクトは、bind
メソッドの第 1 引数として渡された値を内部プロパティ [[BoundThis]]
に保持しています。 そして、内部メソッド [[Call]]
として、通常の関数オブジェクトとは異なるものをもっています。 この [[Call]]
メソッド (ES-15.3.4.5.1) の処理を見ると、内部プロパティ [[BoundThis]]
を this
値として、呼び出し対象関数オブジェクトの [[Call]]
メソッドを呼び出しています。 結局のところ、bind
メソッドの第 1 引数として渡された値がそのまま this
値として渡されるわけですね。
new
演算子によるコンストラクタ呼び出し
ECMA-262 5.1th の 11.2.2 を見ると、new
演算子の右辺に書かれた関数オブジェクトの内部メソッド [[Construct]]
が呼び出されることがわかります。 ECMA-262 5.1th の 13.2.2 を見ると、[[Construct]]
の中で新たに生成されたオブジェクトが、最終的に関数の [[Call]]
メソッドに this
値として渡されることがわかります。
その他
その他に関数オブジェクトの内部メソッド [[Call]]
が呼ばれることってあるんだろうか。 わかりません。
まとめ
まとめというか、個人的に JavaScript を書くときに this
キーワードではまらないように気を付けていること。 細かい挙動は把握してなくていいので、以下のことを守れば this
キーワードではまることはないはずです。
this
キーワードの値は実行コンテキストに結び付けられた値であり、新しく実行コンテキストに処理が移ったときに決定される- グローバルコードでは
this
キーワードの値はグローバルオブジェクト - 関数内では
this
キーワードの値は関数の呼び出され方で決まる - Eval コードではちゃんと理解してない限り
this
キーワードは使うべきでない *7
関数定義時の this
キーワード
- 関数を定義するときには、
- それが
obj.methodName()
形式かnew Function()
形式で呼び出されることが期待される関数であれば、関数コードの中でthis
キーワードを使ってよい - そうでなければ
this
キーワードを使ってはいけない
- それが
関数の扱い方
obj.methodName()
形式で呼び出されることが期待される関数であれば、- 別の関数の引数としてその関数を渡すとか、引数に代入するとか、別のオブジェクトのプロパティにその関数を代入するとかはしてはいけない
- ただし、別のオブジェクトのプロパティに代入することで mix-in のような感じで使うことが想定されている場合はその限りではない
- 関数を呼び出すときは
obj.methodName()
形式で呼び出すこと- ただし、
Function.prototype.call
などでThisBinding
を書きかえる場合はその限りではない
- ただし、
- 別の関数の引数としてその関数を渡すとか、引数に代入するとか、別のオブジェクトのプロパティにその関数を代入するとかはしてはいけない
obj.methodName()
形式で呼び出されることが期待される関数でなければ *8、自由に別の変数に代入したり関数の引数に渡したりしてよい
補足
フィードバックいただいたので紹介します。 ありがとうございます。 (あと思いだしたのでコンストラクタとして使われる場合も追記しました。)
「hoge.huga.bar()はvar foo = hoge.huga;foo.bar()と呼び出してよい」ってルールもある方が親切かも // JavaScript の this キーワードに結びつけられる値はどのように決定されるのか bit.ly/YKGMQV
— kyo agoさん (@kyo_ago) 2013年2月2日
まあ hoge.fuga.bar
という関数を別の変数やプロパティに代入しなければ、たとえ hoge.fuga
をどこか (例えば foo
という変数) に代入しても foo.bar
って呼び出すことになるから、上に書いた指針の範疇ではあるのですが。
あわせて読みたい
- JavaScript の this キーワードは何を指すのか - コールバック関数内では this を使ってはいけない : 関数呼び出し時に
this
キーワードがどのように決定されるのかについて; 今回の記事のベースとなった記事 - ECMAScript のレキシカル環境 ~catch 節のスコープのお話~ : 実行コンテキストが管理する
ThisBinding
以外のものに関する話 (スコープとか)
*1:サンプルコード読めばわかるとはいえ
*2:なんかこれ英語間違ってるような気がする。
*3:詳細は ECMA-262 5.1th の 10.3 節 を見てください
*4:あるいは現在の実行コンテキストに関連する実行可能コードとは別の
*5:Function.prototype.bind
メソッドにより生成された関数オブジェクトの内部メソッド [[Call]]
は、通常の関数オブジェクトの内部メソッド [[Call]]
とは異なっている
*6:簡単にいうと、Reference 型っていうのは ECMA-262 で仕様説明のために使われている型であり、識別子やプロパティの名前解決の際にこの型の値が返されます。 Reference 型の値は、名前解決の対象となった名前に結び付けられた値が保持されている場所 (base という; 名前解決の対象がプロパティであればそのプロパティをもつオブジェクト、対象が識別子であれば Environment Record、など) や名前解決の対象となった名前などをもちます。 GatBase() で取得できる値が、この base です。
*7:そもそも eval
関数を気軽に使うべきではない
*8:すなわち this
キーワードが使われていない関数か new Function()
形式で呼び出されることが期待される関数なのであれば