まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。
この広告は、90日以上更新していないブログに表示しています。
最近JavaScript のthis キーワードについての記事をいくつか見かけて 「そういや自分も昔this キーワードについて記事を書いたなー」 と思って古い記事 を見返してみたのですが、関数呼び出しのことしか説明してなかったので改めてthis キーワードの全般的な話を書いておこうかと思います。 本記事はECMA-262 5.1th に基づいています。 初心者向けのthis キーワードの使い方の指針を示しているわけでもJavaScript 処理系の実装の説明をしているわけでもなく、JavaScript 言語コアの仕様を説明していることに注意してください。
this キーワード周りの指針この記事の最後の 「まとめ」 に、自分が JS 書くときにthis キーワードではまらないようにするための指針を書いてますのでよければご覧ください。
this キーワードについて; 混乱しやすい箇所について思考の流れがわかる感じで説明されてるのでJava とかから JS に来た人には良さそう; 厳密さに欠けるのはちょっと気になるthis の値が決まる」 というような説明があればもっとわかりやすい気がしたthis キーワードとは何かそもそもthis キーワードとは何かという話。
ECMA-262 には、“The
this keyword evaluates to the value of theThisBinding of the current execution context.” (ES-11.1.1) と書かれています*2。 つまり、this キーワードというのは実行コンテキストに結びついているものなわけです。
では実行コンテキスト (ES-10.3) というのは何なのかという話になるわけですが、要は実行可能コードに関する変数の管理をするものと思えば良いでしょう*3。 例えば、関数内で定義された変数は、その関数コードに結びついている実行コンテキスト内で管理されています。 そして、実行コンテキストによって管理されるもののうちの 1 つにthisBinding があるわけです。
実行コンテキストが生成されるのは、新しい*4 実行可能コードに制御が移るときです。 そして、そのときにThisBinding の値がセットされます (ES-10.4)。
ここで説明したことを簡潔に言うと、『this キーワードを評価したときの値は、そのthis キーワードが使われている実行可能コードに制御が移ったときに決定されるものである』 ということになります。
this の値の決定方法ECMAScript においては、実行可能コードには 3 種類あります (ES-10.1)。
まあ読んで字のごとくだと思うので説明はしません。 詳細はECMA-262 を見てください。 ここでは、それぞれの実行可能コードにおけるthis キーワードの値 (すなわちThisBinding の値) の決められ方について説明します。
ThisBindingグローバルコードの実行コンテキストに制御が入ったとき (ES-10.4.1) には、ThisBinding にグローバルオブジェクトがセットされます (ES-10.4.1.1)。
ThisBindingeval 関数の呼び出し側のコンテキスト (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 キーワードの値になるわけではない、ということです。
通常の関数呼び出しの式がどのように評価されるかについては、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 となります。 一方でvarfunction = 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 キーワードの値は関数の呼び出され方で決まるthis キーワードは使うべきでない*7this キーワードobj.methodName() 形式かnew Function() 形式で呼び出されることが期待される関数であれば、関数コードの中でthis キーワードを使ってよいthis キーワードを使ってはいけないobj.methodName() 形式で呼び出されることが期待される関数であれば、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 って呼び出すことになるから、上に書いた指針の範疇ではあるのですが。
this キーワードがどのように決定されるのかについて; 今回の記事のベースとなった記事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() 形式で呼び出されることが期待される関数なのであれば
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。