読者です 読者をやめる 読者になる 読者になる

ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

JavaScript の this キーワードに結びつけられる値はどのように決定されるのか (言語仕様の説明)

JavaScript ECMAScript

最近 JavaScriptthis キーワードについての記事をいくつか見かけて 「そういや自分も昔 this キーワードについて記事を書いたなー」 と思って 古い記事 を見返してみたのですが、関数呼び出しのことしか説明してなかったので改めて this キーワードの全般的な話を書いておこうかと思います。 本記事は ECMA-262 5.1th に基づいています。 初心者向けの this キーワードの使い方の指針を示しているわけでも JavaScript 処理系の実装の説明をしているわけでもなく、JavaScript 言語コアの仕様を説明していることに注意してください。

初心者向けの this キーワード周りの指針

この記事の最後の 「まとめ」 に、自分が JS 書くときに this キーワードではまらないようにするための指針を書いてますのでよければご覧ください。

最近見た記事について

  • JavaScript の this を理解する - tacamy memo : Java ユーザーから見た JavaScriptthis キーワードについて; 混乱しやすい箇所について思考の流れがわかる感じで説明されてるので 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 であれば、thisArgThisBinding になる
  • 2. そうでない場合で、thisArgnullundefined であれば、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 の場合)、thisValueundefined となります。

ここで決定した thisValue が、this 値として内部関数 [[Call]] に渡されます。

Function.prototype.call メソッドや Function.prototype.apply メソッド

ECMA-262 5.1th の 15.3.4.4ECMA-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.fuga.bar という関数を別の変数やプロパティに代入しなければ、たとえ hoge.fuga をどこか (例えば foo という変数) に代入しても foo.bar って呼び出すことになるから、上に書いた指針の範疇ではあるのですが。

あわせて読みたい

*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() 形式で呼び出されることが期待される関数なのであれば