🔁

for 文の let 宣言のスコープの謎 ~どうもシンプルに while に直すことができないらしい~

2024/12/13に公開

はじめに

for 文は while に直すことができると聞きますが 例えば次の様な コードがあったとします。

for (let i = 0, imax = 5; i < imax; i++) {
  console.log('c', i);
  setTimeout(() => console.log('t', i));
}

https://www.typescriptlang.org/play/?ssl=4&ssc=2&pln=1&pc=1#code/GYewTgBAFANgpgFwgSwgXggBgDQoLYCGAHuhAKwDcKEAPPsVcgNRMCUEA3gLABQEEAYxAA7AM4h4AOhggA5lADkAhbmSsKvfqMQAVZHjggArgihR2aAHyCR4qTPkKEKlK3W8AvkA

結果は次の様になります

c 0
c 1
c 2
c 3
c 4
t 0
t 1
t 2
t 3
t 4

うまくいかない while への直し方

while に直してみましょう

こうでしょうか?

{
  let i = 0, imax = 5;
  while (i < imax) {
    console.log('c', i);
    setTimeout(() => console.log('t', i));
    i++;
  }
}

https://www.typescriptlang.org/play/?#code/N4WAUABBA2CmAuECWEC8EAMAaZBbAhgB5oQCsA3OFAO4AWScEAFCgDx5ECUEokUEAYwD2AOwDOQuADpoQgOZMA5AMU4knSnyhiEAFSS5YQgK7wmTbqgB8g0ROmyFi+KuScNVfkgDU3zVABfcACgA

c 0
c 1
c 2
c 3
c 4
t 5
t 5
t 5
t 5
t 5

どうも i の 宣言を while の外にしてしまうと スコープが広すぎて setTimeout に渡している変数の値が動的に変わって最終値になってしまうみたいです。

うまくいった while への直し方

つまりこれと同じ

{
  let i_ = 0, imax = 5;
  while (i_ < imax) {
    let i = i_;
    console.log('c', i);
    setTimeout(() => console.log('t', i));
    i_++;
  }
}

https://www.typescriptlang.org/play/?#code/N4WAUABBA2CmAuECWB9CBeCAGANMgtgIYAeGEArANzhQDuAFknBABSoQA8BJAlBKJCgwEyMqmqCoAYwD2AOwDOMuADpoMgOYsA5FO14kPCUIgKEAFST5YMgK7wWLPugB8EWYuWw1mnfH3IPEY0QqgA1GHGEAC+4NFAA

c 0
c 1
c 2
c 3
c 4
t 0
t 1
t 2
t 3
t 4

よしうまくいきました。

おわりに

つまり for は while にシンプルに直すことはできない でした。

実態のスコープ的には for 文中の let 宣言しても何かシンプルじゃないスコープ(ブロック一枚噛ませてる?)があるのでは?といった感じがありました。

以上。

余談

もしも for 文で var で宣言すると次の様になります。

for (var i = 0, imax = 5; i < imax; i++) {
  console.log('c', i);
  setTimeout(() => console.log('t', i));
}

https://www.typescriptlang.org/play/?#code/GYewTgBAFAbghpAlhAvBADAGgogtnAD1QgFYBuHCAHh3wIsQGpGBKCAbwFgAoCCAYxAA7AM4gANgFMAdOJABzKAHJ+S7IhZkefEZIAuAFTySQAVz1QobFAD4BwsVNkLletThaaeAXyA

c 0
c 1
c 2
c 3
c 4
t 5
t 5
t 5
t 5
t 5

実行結果は予想の通りです。i の宣言 が function の先頭になる(つまりfunction scopeな)ので for の外で宣言したのと同等となり、こうなります。かわいいですね。

余談 その 2

for 文の説明自体は 14.7.4.2 Runtime Semantics: ForLoopEvaluation にあるのですが、

https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forloopevaluation

var 宣言じゃない方の 手動 for 文 ForStatement : for ( Expressionopt ; Expressionopt ; Expressionopt ) Statement
var 宣言の方の 手動 for 文 ForStatement : for ( var VariableDeclarationList ; Expressionopt ; Expressionopt ) Statement と明示的にわけているところをみるとスコープ回りの挙動をそこで制御しているのかなといった感じはします。

ForStatement : for ( Expressionopt ; Expressionopt ; Expressionopt ) Statement

  1. If the first Expression is present, then
    a. Let exprRef be ? Evaluation of the first Expression.
    b. Perform ? GetValue(exprRef).
  2. If the second Expression is present, let test be the second Expression; otherwise, let test be empty.
  3. If the third Expression is present, let increment be the third Expression; otherwise, let increment be empty.
  4. Return ? ForBodyEvaluation(test, increment, Statement, « », labelSet).

https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#prod-qUCVnDNm

ForStatement : for ( var VariableDeclarationList ; Expressionopt ; Expressionopt ) Statement

  1. Perform ? Evaluation of VariableDeclarationList.
  2. If the first Expression is present, let test be the first Expression; otherwise, let test be empty.
  3. If the second Expression is present, let increment be the second Expression; otherwise, let increment be empty.
  4. Return ? ForBodyEvaluation(test, increment, Statement, « », labelSet).

https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#prod-HwfL-Win

Discussion