知りたい事

// module.js
console.log(this);// ???

Babel 前提

  • Babelは入力されたコードをES6 moduleとして扱う

Babel assumes that all input code is an ES6 module

(結論的には--blacklist strictでこの挙動は無効化できる)

ES6 moduleの前提知識

  • Environment Recordというのはそのスコープと変数を関連付けたりするような環境情報的なものを入れる場所
    • 関数とかモジュールとかwith用みたいな何種類かある
  • ModuleはModule Environment Recordsのを作ってそこに情報を記録する
  • Module Environment Recordsはdeclarative Environment Recordの一種で、加えてModule専用の記録領域がある(importのbindingのための場所)
  • 基本的な仕組みはdeclarative Environment Recordと同じ

(Recordの所は実装してみないとイマイチピンと来ないので大分ぼやかしてます。 Javaの実装見るとModuleEnvironmentRecordはDeclarativeEnvironmentRecordを継承するみたいなイメージでも良さそう)

ES6 thisの前提知識

thisという値がどう解決されるか

のアルゴリズムによって決定される。

2つをまぜると以下のような感じの構造

  1. Let envRec be GetThisEnvironment( ).
    1. Let lex be the running execution context’s LexicalEnvironment. // <= 今いるEnvironment(スコープみたいなもの)と取り出す
    2. Repeat
      1. Let envRec be lex’s EnvironmentRecord.
      2. Let exists be envRec.HasThisBinding().// <= そのEnvironment RecordsのHasThisBinding()を見る
      3. If exists is true, return envRec. // <= trueを返してきたら*へ
      4. Let outer be the value of lex’s outer environment reference.
      5. Let lex be outer.(Repeatへ戻る)
  2. Return envRec.GetThisBinding(). // *<= そのEnvironment RecordsのGetThisBinding()がthisの値になる

擬似コード

function GetThisEnvironment(){
    var lex = currentLexicalEnvironment;
    while(true){
	    var envRec = lex.EnvironmentRecord;
	    if(envRec.HasThisBinding()){
	        return envRec;
	    }
	    lex = envRec.outer;
	    // 最後はglobalがlexに入る
    }
}
var envRec = GetThisEnvironment();
envRec.GetThisBinding(); // thisの値

簡単にいえば、そのスコープがHasThisBinding()でtrueを返すならGetThisBinding();の返り値がthisの値になる。


Moduleのthis

Moduleはどこで作られるかというのは8.1.2.6 NewModuleEnvironment (E)で定義されている。

この内部メソッドを呼び出してるのは、15.2.1.16.4 ModuleDeclarationInstantiation( ) Concrete Methodしかないので、

NewModuleEnvironment(realm.[[globalEnv]])

先ほどのmodule Environment Recordのouterにはglobal enviromentが入る。(prototypeチェーンみたいな話)

ModuleのGetThisEnvironment

先ほど書いたように、thisの値は、そのEnvironment(スコープ)のenv.GetThisBinding()env.HasThisBinding()で決まる。

moduleのtop levelのコードから直近のEnvironmentはmodule Environmentなので、module Environmentの定義をみる。

moduleEnvRecord.HasThisBinding()がtrueを返すなら、moduleEnvRecord.GetThisBinding()thisの値になると言える。

8.1.1.5 Module Environment Recordsで

に書いてあるように、moduleEnvRecordのHasThisBinding()はtrueを返す。 つまり、moduleEnvRecordのGetThisBinding()thisの値になるので、moduleのthisはundefeinedになる。 という結論

8.1.1.5.3 HasThisBinding ()

Module Environment Records provide a this binding.

    Return true.

8.1.1.5.4 GetThisBinding ()

    Return undefined.

結論

BabelがコードをES6 moduleとして扱うなら、top levelのthisがundefinedになるのは仕様。

Babelにissueを立てるならば、なぜ全てをES6 moduleとして扱うのか?が焦点。

また、この挙動は--blacklist strictで無効化するオプションが用意されている。

$ babel --blacklist strict script.js

であってると思うのだけれど、間違っているなら修正したいのでIssuesPull Requestsお願いします。

おまけ

ES6 DraftはRev 38もあるぐらい結構更新されてFinal Draftが今でてる感じです。 Moduleは特に色々変更があったと思うので、diffからこれが何時入ったのか見てみます。

答え合わせ

The this binding at the top level of a module has the value undefined.

December 6, 2014 Draft Rev 29の変更点

diffがハイライトされたpdf with rev29 change markupで確認できる。

img

テキストDiffを確認

テキストベースでES6 Draftの各Rev毎をGitのコミットにして検索できる仕組みを作りました。

git clone https://github.com/meta-ecmascript/es6-draft-revision.git
cd es6-draft-revision
git show rev29 | grep "return undefined" -i -C 10 | grep "CreateImportBinding" -B 14

output:

--
--
 deletable.

+HasThisBinding ()
+
+Module Environment Records provide a THIS binding.
+
+1.  Return TRUE.
+
+GetThisBinding ()
+
+1.  Return UNDEFINED.
+
 CreateImportBinding (N, M, N2)
--

 The concrete Environment Record method CreateImportBinding for module

git logから検索

コミットの差分から検索したかったけど、何か上手くマッチしない…

git log --perl-regexp -i -G "GetThisBinding(.|\n)*?Return UNDEFINED"

--perl-regexpでperlの正規表現使えた方がいいので、gitを入れるときにbrew install git --with-pcreで入れてる。