JavaScriptで、なぜfor文の初期化部分においてletで宣言された変数はループごとに異なるインスタンスを持ちうるのか? について調べた。
発端
社内のslack内での@i9iの発言による。
確かにJavaやCだと、変数iの一つのインスタンスが逐次インクリメントされるようにコンパイルされるので、そのような疑問が上がるのも自然である。
調査
理解できていなかったので、規格のfor文の記載を調べる。
14.7.4.2 Runtime Semantics: ForLoopEvaluation
3通りのfor文があるが、
for ( Expression opt ; Expression opt ; Expression opt ) Statement
for ( var VariableDeclarationList ; Expression opt ; Expression opt ) Statement
for ( LexicalDeclaration Expression opt ; Expression opt ) Statement
今回は3つ目のfor文が該当する。
for文の評価
for文は以下のように評価される。
(リンク先の方が引用より読みやすいので参照されたい)
ForStatement : for ( LexicalDeclaration Expressionopt ; Expressionopt ) Statement
- Let oldEnv be the running execution context's LexicalEnvironment.
- Let loopEnv be NewDeclarativeEnvironment(oldEnv).
- Let isConst be IsConstantDeclaration of LexicalDeclaration.
- Let boundNames be the BoundNames of LexicalDeclaration.
- For each element dn of boundNames, do
a. If isConst is true, then
i. Perform ! loopEnv.CreateImmutableBinding(dn, true).
b. Else,
i. Perform ! loopEnv.CreateMutableBinding(dn, false).- Set the running execution context's LexicalEnvironment to loopEnv.
- Let forDcl be the result of evaluating LexicalDeclaration.
- If forDcl is an abrupt completion, then
a. Set the running execution context's LexicalEnvironment to oldEnv.
b. Return Completion(forDcl).- If isConst is false, let perIterationLets be boundNames; otherwise let perIterationLets be « ».
- Let bodyResult be ForBodyEvaluation(the first Expression, the second Expression, Statement, perIterationLets, labelSet).
- Set the running execution context's LexicalEnvironment to oldEnv.
- Return Completion(bodyResult).
ここで、2.でNewDeclarativeEnvirionment(oldEnv)
が実行されている。これは
- Let env be a new declarative Environment Record containing no bindings.
- Set env.[[OuterEnv]] to E.
- Return env.
という動作をし、Environment Recordがループに入った時点で作られる(Environmentについては後述)。
その後、4., 5.で、2.で作ったEnvironment上にLexicalDeclaration各宣言に対応する識別子とそれに対応する値が作られる。これはCreateMutableBinding()
で行われる(Binding: 名前束縛(Wikipedia))。
このEnvironmentは6.において、実行コンテキストのLexicalEnvironmentとなる。
その後10.でループに入る。
forループ内部の処理
ループ内の処理は14.7.4.3 ForBodyEvaluation(test, increment, stmt, perIterationBindings, labelSet)である。
- Let V be undefined.
- Perform ? CreatePerIterationEnvironment(perIterationBindings).
- Repeat,
a. If test is not [empty], then
i. Let testRef be the result of evaluating test
ii. Let testValue be ? GetValue(testRef)
iii. If ! ToBoolean(testValue) is false, return NormalCompletion(V).
b. Let result be the result of evaluating stmt.
c. If LoopContinues(result, labelSet) is false, return Completion(UpdateEmpty(result, V)).
d. If result.[[Value]] is not empty, set V to result.[[Value]].
e. Perform ? CreatePerIterationEnvironment(perIterationBindings).
f. If increment is not [empty], then
i. Let incRef be the result of evaluating increment.
ii. Perform ? GetValue(incRef).
2.および3-e.でCreatePerIterationEnvironment(perIterationBindings)
が実行され、その中は以下。
14.7.4.4 CreatePerIterationEnvironment(perIterationBindings)
- If perIterationBindings has any elements, then
a. Let lastIterationEnv be the running execution context's LexicalEnvironment.
b. Let outer be lastIterationEnv.[[OuterEnv]].
c. Assert: outer is not null.
d. Let thisIterationEnv be NewDeclarativeEnvironment(outer).
e. For each element bn of perIterationBindings, do
i. Perform ! thisIterationEnv.CreateMutableBinding(bn, false).
ii. Let lastValue be ? lastIterationEnv.GetBindingValue(bn, true).
iii. Perform thisIterationEnv.InitializeBinding(bn, lastValue).
f. Set the running execution context's LexicalEnvironment to thisIterationEnv.
2. Return undefined.
1-a.で実行中のLexicalEnvironmentをlastIterationEnvとしている。ループの最初ならループ内の2.で作成したEnvironmentとなり、ループ途中だとひとつ前のループで作成したEnvironmentとなる。
1-d.でNewDeclarativeEnvirionment(outer)
しており、ここに1-e-i.で新たに作成したEnvirionmentでCreateMutableBinding()
を実行している。これにより、perIterationBindingsの各識別子、すなわちfor文の初期化部分でlet宣言した変数の識別子、ごとに対応する値が作られる。すなわち、ループごとに識別子と対応する値が、異なるEnvironmentの中にそれぞれ異なるインスタンスとして作られる。
またここで、outerは1-b.により、ひとつ前のループのouterとなる。ループ最初の場合のouterはループ外のEnvironmentなので、以後もouterはループ外のEnvironmentを参照し続ける。
Environment Records
ここでEnvironment: 環境であるが、環境とはEnvironment Recordsで説明される。
以下は簡単な環境の説明である。
function f() {
let i = 1;
function g() {
let j = 2;
return i + j;
};
return g;
}
で表され、実行中に参照できる識別子と、その識別子に対応する値の組である。
実行中の関数からは、その実行中の関数を定義した関数内(外側の環境)の変数も自由変数として参照できるため(レキシカルスコープ)、画像のような構造となる。
ループ内部の処理中にあったCreatePerIterationEnvirionment(outer)
は、outer(前のループ時の環境の「外側の環境」)を「外側の環境」として新しい環境を作成しているため、ループごとの環境は
のようになる。
結論
このようにして、ループ初期化部分でletで宣言された変数は、ループごとに環境が作成され、そこに変数と値が作成されるので、それぞれ異なるインスタンスとなる。
これにより、有名なfor文初期化部分でのvarによる宣言とletによる宣言の挙動の違い
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(() => console.log(i));
}
functions[0]();
functions[1]();
functions[2]();
の結果が
3
3
3
となり、
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(() => console.log(i));
}
functions[0]();
functions[1]();
functions[2]();
の結果は
0
1
2
となる点も理解できる。三つあったfor文のうち、二つ目の文との処理の違いを追いかければ明らかである。
(続く)