Movatterモバイル変換


[0]ホーム

URL:


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

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

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

この広告は、90日以上更新していないブログに表示しています。

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

最近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 には、“Thethis 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 キーワードが使われている実行可能コードに制御が移ったときに決定されるものである』 ということになります。

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 となります。 一方で
varfunction = 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.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 って呼び出すことになるから、上に書いた指針の範疇ではあるのですが。

あわせて読みたい

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

注目記事
検索
最近のコメント
    カテゴリー

    引用をストックしました

    引用するにはまずログインしてください

    引用をストックできませんでした。再度お試しください

    限定公開記事のため引用できません。

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

    [8]ページ先頭

    ©2009-2025 Movatter.jp