意外と複雑！for 文のスコープに関するクイズ９問【解説付き】

# イントロダクション

ES6 で `const/let` が追加され、ブロックスコープの変数を宣言できるようになった。これにより `for` ループのループ変数は `var` ではなく `let` で宣言することが多くなったが、実は **`let` で宣言されたループ変数のスコープは意外なほど複雑**である。このあまり知られていない `for` 文の振る舞いは、非同期関数やクロージャーを用いると浮かび上がってくる。この記事ではそのような問題を取り上げる。

各コードについて、実行したときに**何がどの順序で出力されるか**を考えてみよう。実行環境はブラウザでも Node.js でも同じ結果になるはずである。

**※問題ごとの解説は軽い説明で済ませているが、最後にまとめて for ループの実行の仕組みを説明している。**

なお、前回は [Promise に関するクイズ13問](https://qiita.com/righteous/items/1d6e0396efd43d130a38) という記事を書いたので興味があればぜひどうぞ(今回も `setTimeout` が登場するので関係はある)。

# 問題 V-1: Easy

```js
for (var i = 0; i < 3; i++) {
}

try {
  console.log(i)
} catch (e) {
  console.log("error")
}
```

## 解答

```
3
```

`var` で宣言したループ変数は、`var` である限り function scope であるため、for-loop の前後でもアクセスすることができる。つまり問題の for ループは下のコードと完全に同値である。

```js
var i = 0
for (; i < 3; i++) {
}
```

for ループは `i` が `3` になったときに `i < 3` が `false` になって終了するので、`3` が出力される。

# 問題 V-2: Normal

```js
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
```

## 解答

```
3
3
3
```

これはよく初心者が意図せず書いてしまうコードであり、例えば `button` 要素の配列の各要素にイベントリスナーの追加をしたいときなどにこのパターンに遭遇する。

ループ変数 `i` は一つしか存在せず、3つのコールバック関数 `() => console.log(i)` はすべてこの変数を参照している。そしてこれらの関数が実行されるタイミングは for ループが終了したあとなので、`i` の値は `3` になっている。

# 問題 L-1: Easy

```js
for (let i = 0; i < 3; i++) {
}

try {
  console.log(i)
} catch (e) {
  console.log("error")
}
```

## 解答

```
error
```

`let` で宣言したループ変数は for ループ内でしかアクセスできない。

# 問題 L-2: Normal

```js
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
```

## 解答

```
0
1
2
```

ボディ `{...}` の部分が3回実行されるが、**毎回別々の変数 `i` が作られる**というのがポイントである。そのため、3つのコールバック `() => console.log(i)` はそれぞれ別々の変数 `i`(=0) `i`(=1) `i`(=2) を参照することになる。

同じ timeout でセットされた `setTimeout` コールバックは、セットされた順番で実行されるので、普通に `0 1 2` の順番で出力される。

`let` を用いることで問題 V-2 のような現象を回避することができる、ということである。

# 問題 L-3: Normal

```js
for (let i = 0; i < 10; i++) {
  console.log(i)
  i += 1
}
```

## 解答

```
0
2
4
6
8
```

毎回別々の変数 `i` が作られはするが、`i` の値を途中で変えれば次の `i` にも影響する。なぜなら、ボディを実行した**あと**のループ変数 `i` の値が**コピー**されて新しい `i` が作られ、`i++` が実行されるからである。

この問題に関しては `var` でも同じ結果になる。

# 問題 L-4: Normal

```js
for (let i = 0; i < 10; i++) {
  setTimeout(() => console.log(i), 0)
  i += 1
}
```

## 解答

```
1
3
5
7
9
```

5つのコールバック `() => console.log(i)` が作られるが、それぞれ別々の `i` を参照しているのはここまでで説明した通りである。それぞれの変数の値は以下のように変わる。

- 1つ目の `i`: 0 → 1
- 2つ目の `i`: 2 → 3
- 3つ目の `i`: 4 → 5
- 4つ目の `i`: 6 → 7
- 5つ目の `i`: 8 → 9

先述の通りコールバックが実行されるのは for ループが実行しおわったあとなので、それぞれ後の値が出力される。


# 問題 L-5: Hard

```js
let f

for (
  let i = (() => {
    f = () => console.log(i)
    return 0
  })();
  i < 3;
  i++
) { }

f()
```

## 解答

```
0
```

for ループの構文の制限上 IIFE を使っているが、要は `f` に代入すると同時に `i` を初期化しているだけである。

この問題のポイントは、**ループ変数の初期化のあとにも変数のコピーが発生する**ことである。そのため、ボディは3回実行されるが、実はループ変数 `i` は4個作られることになる。

初期化のときに作られた `i` の値は for ループが終わった後も `0` であるため、`0` が出力される。

# 問題 L-6: Hard

```js
let f = () => console.log("start")

for (
  let i = 0;
  i < 10;
  i++, f = () => console.log(i)
) {
  i += 1
  f()
}
```

## 解答

```
start
3
5
7
9
```

ポイントは、変数のコピーが発生するのは初期化の直後とインクリメント式 `i++, f = () => console.log(i)` の**直前**であるということである。順を追って考えれば難しくはない。

1. `i` が `0` で初期化される(i1 とする)。
2. i1 がコピーされる(i2 とする)。i2 = 0
3. i2 に対してボディが実行され、i2 = 1 になり、`start` が出力される。
4. i2 がコピーされ、i3 = 1 ができる。
5. `i++, f = () => console.log(i)` が実行され i3 = 2 になる。この `() => console.log(i)` の `i` は i3 を指す。
6. ボディが実行され、i3 = 3 になり `f` が実行され `3` が出力される。
7. i3 がコピーされ、i4 = 3 ができる。
8. `i++, f = () => console.log(i)` が実行され i4 = 4 になる。
9. ボディが実行され、i4 = 5 になり `f` が実行され `5` が出力される。
10. 以下同様

# 問題 L-7: Hard

```js
let f
for (
  let i = 0;
  i < 10;
  i++, f()
) {
  i += 1
  f = () => console.log(i)
}
```

## 解答

```
1
3
5
7
9
```

これも L-6 と同じように考えればよい。`f` が代入/実行されるタイミングが変わっただけである。

1. `i` が `0` で初期化される(i1 とする)。
2. i1 がコピーされる(i2 とする)。i2 = 0
3. ボディが実行され、i2 = 1 になり、**`f` の関数の `i` は i2 を指す**。
4. i2 がコピーされ、i3 = 1 ができる。
5. `i++, f()` が実行され、i3 = 2 になる。**i2 = 1 なので `1` が出力される**。
6. ボディが実行され、i3 = 3 になり、**`f` の関数の `i` は i3 を指す**。
7. i3 がコピーされ、i4 = 3 ができる。
8. `i++, f()` が実行され、i4 = 4 になる。**i3 = 3 なので `3` が出力される**。
9. 以下同様

# ちゃんとした説明

## Lexical Environment (LE)

実行中の変数の値は、スコープごとに別々のテーブルで保存されている。このテーブル一つ一つを lexical environment と呼ぶ。

筆者の別記事で詳しく説明しているので参考にしていただきたい。クロージャーが実現できるのもこの LE のおかげである。

👉 [【図解】コールスタックとクロージャーを理解する](https://qiita.com/righteous/items/494340cf16c7a35f742e)

## `var` の場合

`var` で変数を宣言した場合、話は単純である。下のようにループ変数 `i` を宣言した場合、そのスコープは `for` の外の変数 `outer` と同じである(= 同じ lexical environment に属する)。`{ ... }` の部分はブロックなので、そこでまた新しいスコープが必要になり、ループのたびに LE が作成されることになる。

```js
const outer = 0

for (var i = 0; i < 3; i++) {
  const inner = 0
  setTimeout(() => console.log(i), 0)
}
```

変数が参照されるとき、LE のチェーン(黒の矢印)をたどって探索が行われる。3つの関数が同じ `i` を参照していることが分かるだろう。

![for-loop execution context.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/131132b7-8c34-e36a-7e5a-79df1555852e.png)

## `let` の場合

`let` で宣言されたループ変数は **for ループがスコープとなる**。`var` のときと比べると一段階スコープが深くなっていることに注意する必要がある。

また、途中で**ループ変数のコピー (= LE のコピー)** が発生する。これは問題 L-2 のような状況で変数が別々でないと不都合が生じるからである。

結論から言うと、`let` の for ループは次のように実行される。

1. init
2. copy
3. 以下繰り返し
    - test (失敗すれば終了)
    - body
    - copy
    - increment

次のコードを用いて、それぞれのステップを説明する。

```js
const outer = 0

for (let i = 0; i < 3; i++) {
  const inner = 0
  setTimeout(() => console.log(i), 0)
}
```

- init

現在の LE の下に ループ変数用の**新しい LE が作成され**、そこでループ変数が初期化される。
![for-let init](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/190e4ea8-1c43-2c9c-2186-39d275ce1545.png)

- copy

init で作成された **LE がコピー**される。

![for-loop copy1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/d7acb54e-0a2e-4095-57c8-5bad2049137e.png)


- test

新しい LE にて ループの終了条件がチェックされる。

![for-loop test](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/838f541a-6bbe-5771-6d95-a56a4077737a.png)

- body

ボディが実行される。このコードではボディが**ブロック** `{...}` になっているため、**新しい LE が作られる**(勘違いされやすいが、ブロックは for 構文の一部ではない。for ループのボディはステートメントであればなんでもよく、ブロックステートメントである必要はない)。

ブロックの中の2つのステートメントはこの LE で実行される。ここで作られた関数 `() => console.log(i)` は実行中の LE (緑)へのポインタが張られる。

![for-let body](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/9283d553-25a2-a5ec-809e-e855a43aaffb.png)

- copy

ループ変数用の LE が**コピー**される。

![for-let copy](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/6c910723-4b27-00d6-fef9-8d08792ee64c.png)

- increment

新しい LE でインクリメント式が実行される。

![for-loop increment](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/40c9d8a0-5558-6698-7c5f-afaf73e5db50.png)

- test

条件式が評価される。

![for-loop test](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/fe400b32-8410-5a3b-eb77-993e1673a659.png)

- body

先と同様に、ブロックなので新しい LE が作られ、そこでブロック内のステートメントらが実行される。

![for-loop execution context8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/0e30f350-09d0-1d94-bd09-e9feea31825c.png)

- copy

ループ変数の LE がコピーされる。

![for-loop execution context9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/7aa243ba-1872-0900-69b9-b795497e6951.png)

- increment

インクリメント式が評価される。

![context9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/95b0f74d-6c73-32ac-fb76-b4650c36fd3d.png)

以下、test-body-copy-increment が繰り返される。


for ループが終了し、3つの `setTimeout` コールバックが実行される時、下の図のようにそれぞれの `i` が別々の LE に属する `i` を参照することになる。

![name resolution.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/0cf85448-4586-d5bf-cb06-585e2cf1f492.png)

# まとめ

問題 L-3 までは正答できる人も多いと思われるが、L-3 もちゃんと仕組みを説明できる人は少ないだろう。MDN でもこれについての説明は見当たらなかったため、仕様を読んで記事を書くに至った。

重要なのは、`let` の場合はループ変数用の LE が作られて、反復ごとにコピーされることである。コピーされるタイミングを知らないと答えられないような作為的なコードを問題にしたが、普通のコードを書く上ではそこまで知っておく必要はないだろう。

このような for の複雑な振る舞いについて考えたくない場合は、for-of を使うことをおすすめする。

# 参考
- [(ECMAScript) The for Statement](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-for-statement-runtime-semantics-labelledevaluation)
- [(YouTube) JavaScript for-loops are… complicated - HTTP203](https://www.youtube.com/watch?v=Nzokr6Boeaw)