`let`にあんまり知られてなさそうな性質が結構あった話

# 前書き

```javascript:A
for (var i = 0; i < 5; i++) {
	setTimeout(() => {
		console.log(i);
	}, 100);
}

// 5 5 5 5 5
```



```javascript:B
for (let i = 0; i < 5; i++) {
	setTimeout(() => {
		console.log(i);
	}, 100);
}

// 0 1 2 3 4
```
この2つのコード、`var`と`let`の違いとしてそこそこ有名なものだと思います。

この動作自体に文句はありませんし、自分もそういうもんだと思って使っていたのですが･･･

よくよく考えると**B**のコード上では

1. `console.log(i)`は`let`で宣言された変数`i`を参照している
2. `let`で宣言された変数`i`は、`for`文によってインクリメントされている
3. `console.log(i)`が実行されるのは、`for`ループが完了した後である

という処理になっているため、**A**のコードと同じ結果になるはずです。

[MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/let)にあるような`let`の性質だけでは、
この部分の説明がつかなくて気になってしまったというのが発端で、
色々試していた＋調べていたらそこそこな量になったのでまとめました。

ただ、知られてなさそうな性質として気になったのが全て
`for`と組み合わせた時に表れる性質なので
**実用性はそんなにないと思います。**


# よく知られているであろう性質

## ブロックスコープである

```javascript
{
	let a = 1;
	console.log(a); // 1
}
console.log(a);

// Uncaught ReferenceError: a is not defined
```

`let`宣言するとスコープがブロック内に制限されるやつです。

どこのサイトでも載ってる基本中の基本。

## 同一ブロック内での再宣言不可

```javascript
{
	let a = 1;
	let a = 2;
}

// Uncaught SyntaxError: Identifier 'a' has already been declared
```
`let`宣言すると、同一スコープで再宣言ができません。
これは`var`での再宣言でも同じくエラーになります。

```javascript
{
	let a = 1;
	{
		let a = 2;
		console.log(a);
	}
	console.log(a);
}

// 2 1
```

のように、ブロック内の別ブロックであれば
再宣言ではなく新規の変数宣言扱いになるので、エラーは出なくなります。

これも基本。

## `for`文のループブロックは、それぞれが別のブロックスコープになる

```javascript
for (let i = 0; i < 5; i++) {
	let a = i;
	console.log(a);
}

// 1 2 3 4 5
```

仮に1つの`for`文における全ループブロックが単一のスコープであれば
`let a`での再宣言でエラーが発生しているはずですが、
エラーは発生していないのでそういうことなのです。

この辺りまでは調べればそこそこ出てきます。

## 巻き上げ的な挙動はあるが、(巻き上げが問題になるようなコードであれば)エラーになる

```javascript
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a;
```

[MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/let)
> undefined の値で始まる var 変数と異なり、 let 変数は定義が評価されるまで初期化されません。変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。

定義を評価というのが具体的に何を指すのかちょっと曖昧な気もしますが

```javascript
console.log(a);

// ひとつ上のコードとはちょっと違い、変数が存在しないエラー
// Uncaught ReferenceError: a is not defined
```

```javascript
let a;
console.log(a);

// undefined
```

という挙動から考えると、**`let`宣言された行に到達＝定義を評価**と考えて良さそうです。
宣言自体は巻き上がっておらず、ブロック開始時点で宣言前の準備が行われている、という感じですね。

いずれにしても、このエラーが発生するようなコードには構造上の問題があると考えた方が良いと思います。

これを知ってたら中級くらい？　実務上で遭遇することは結構ありそう。

# あんまり知られてなさそうな性質

本題です。
冒頭にも書きましたが、以下は**`for`文との組み合わせの際に表れる性質**です。

## 「`for`文の存在するブロックスコープ」と「`for`ループブロックスコープ」の間に、見えないブロックスコープが存在する（ような挙動をする）

```javascript
{
	// スコープ①
	for (let i = 0; i < 10; i++) {
		// スコープ②
		i++;
		console.log(i);
	}
	console.log(i);
}

// Uncaught ReferenceError: i is not defined
```

エラーが出ていることから、`for`文の初期化部は**スコープ①**ではないことが分かります。

仮に**スコープ②**であったとしたら、各ループブロック内での`let i`宣言はブロックごとに個別の変数になるので、
`i++`でループ数が減少する説明がつきません。

なのでイメージとしては

```javascript
{
	{
		let i;
		for (i = 0; i < 10; i++) {
			i++;
			console.log(i);
		}
	}
	console.log(i);
}
```
のようなコードの挙動が近しいです。

**※このコードはあくまでイメージです。
　後述する別の性質を破壊してしまうので、使ってはいけません。**

## `for`文において、ある条件下でのみ「別の変数の参照（のような挙動）」が起こる

発端となった挙動の原因です。

### なぜこうなるのか

それらしい記載のあるサイトがあまりなかったのですが

- https://hacks.mozilla.org/2015/07/es6-in-depth-let-and-const/

> つまり、for (let...)ループが複数回実行され、そのループにクロージャーが含まれている場合（話している猫の例のように）、すべてのクロージャーが同じループ変数をキャプチャするのではなく、各クロージャーがループ変数の異なるコピーをキャプチャします。

とのこと。

発生する条件は異なりますが、関数内で`arguments`という変数が自動で使えるようになっていたり、
状況に応じたオブジェクトが`this`に束縛されているのに近しいイメージでしょうか。

確かに

```javascript
for (let i = 0; i < 5; i++) {
	setTimeout(() => {
		i *= 2;
		console.log(i);
	}, 100);
}

// 0 2 4 6 8
```

と、各クロージャ内での`i`は、それぞれ別の`i`として存在しているように見えます。

これなら確かに**B**の挙動になるのも頷けるのですが･･･

### 色々試してみる

色々なコードを試してみるうちに、この説明もちょっと違っている気がしてきました。
（5年前の記事なので、当時とパーサの解釈が違う可能性はありますが）

#### 即時関数

```javascript
for (let i = 0; i < 5; i++) {
	(() => {
		i++;
		console.log(i);
	})();
}
console.log('end');

// 1 3 5 end
```

クロージャ内の処理ではあるものの、普通に`for`文の初期化部の`let i`を参照しちゃっているようです。

#### 関数定義＋同期実行

```javascript
for (let i = 0; i < 5; i++) {
	function f() {
		i++;
		console.log(i);
	};
	f();
}
console.log('end');

// 1 3 5 end
```

やってることは即時関数と変わらないので、まあこうなるよなといった印象。

#### `Promise`の`executor`

```javascript
for (let i = 0; i < 5; i++) {
	new Promise(resolve => {
		i++;
		console.log(i);
	});
}
console.log('end');

// 1 3 5 end
```

`Promise`と言えど`executor`は同期的に実行されるので、まあこうなるよなといった印象。

#### `Promise.then`

```javascript
for (let i = 0; i < 5; i++) {
	new Promise(resolve => {
		resolve();
	})
	.then(() => {
		i++;
		console.log(i);
	});
}
console.log('end');

// end 1 2 3 4 5
```

動いた。
ほぼ即時的な処理であっても非同期処理であればいいんだろうか？

#### `setTimeout`の`delay = 0`

```javascript
for (let i = 0; i < 5; i++) {
	setTimeout(()=>{
		i++;
		console.log(i);
	}, 0);
}
console.log('end');

// end 1 2 3 4 5
```

こっちも`Promise.then`と同じような処理だしやはり動いた。

#### `setInterval`

```javascript
for (let i = 0; i < 5; i++) {
	let c = 0;
	const p = setInterval(()=>{
		i += 10;
		console.log(i);
		if (c++ === 1) {
			clearInterval(p);
		}
	}, 0);
}
console.log('end');

// end 10 11 12 13 14 20 21 22 23 24
```

同一の関数を複数回実行しても、ちゃんと**それぞれのクロージャごとに個別の`i`**を参照していそう

ここまでくると、**クロージャが非同期処理として実行される**ことが条件のようにも思えますが･･･

#### クロージャを`for`文の全ループの完了後に実行

```javascript
const a = [];
for (let i = 0; i < 5; i++) {
	a.push(function() {
		i++;
		console.log(i);
	});
}
console.log('end');
for (let j = 0; j < a.length; j++) {
	a[j]();
}

// end 1 2 3 4 5
```

非同期処理ではないですが、同期処理としての順番を変えたら参照する変数が変わっているようです。

念のため、外部スコープの配列に格納しているせい、という可能性もあるので･･･

```javascript
const a = [];
for (let i = 0; i < 5; i++) {
	a.push(function() {
		i++;
		console.log(i);
	});
	a[a.length - 1]();
}
console.log('end');

// 1 3 5 end
```

`let i`参照に戻っているので、特に関係はなさそうです。


#### 新たな参照先が生成されるタイミング

クロージャがそれぞれ個別の`i`を生成するタイミングも調べておきます。

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

// 1 3 5
```

ちょっとわかりづらいですが、

1. `let i = 0` (0)
2. クロージャ定義 (0)
3. ループ内最下部の`i++` (1)
4. `for`文の更新部の`i++` (2)
5. クロージャ定義 (2)
6. ループ内最下部の`i++` (3)
7. `for`文の更新部の`i++` (4)
8. クロージャ定義 (4)
9. ループ内最下部の`i++` (5)
10. `for`文の更新部の`i++` (6)
11. `for`文終了 (6)

()内は そのときの`i`の値です。

どうやら、**クロージャが定義された瞬間の`i`**ではなく、
**クロージャ定義後の`i++`が反映された状態の`i`**が新たな参照先変数の値になっているようです。

### 結論

ということで、**B**のような動作をする理由は、

**1. `for`文の初期化部で変数が`let`宣言されている（仮に`let i`とする）
2. `for`ループブロック内で関数（クロージャ）が定義されている (仮に*`f`*とする)
3. *`f`*の内部から、`i`が参照されている
4. `for`ループ処理が完了した後に*`f`*が実行されている**

**の全ての条件を満たした場合**

***`f`*内で参照している`i`の値が、「*`f`*実行時の`i`の値」ではなく「*`f`*が定義されたスコープの末端に到達した時点での`i`の値」に束縛される**

という性質に当てはまっているコードだから、ということなのではないかと思います。

ちなみに条件の`1`についてですが、`let`での宣言を`for`文の初期化部以外にしてしまうと

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

// 5 5 5 5 5 
```

と、クロージャごとに個別の参照先にならなくなります。
あくまで `for`文の初期化部で`let`宣言した時のみ、の性質ですね。

### 起こりえる問題

```javascript
for (let i = 0, finished = 0; i < 5; i++) {
	setTimeout(() => {
		if (++finished === 5) {
			console.log('finished');
		}
	}, Math.random() * 100);
}
```

`for`文の初期化部は独立したスコープになっている、という性質を考えると
**`for`ループ内からのみアクセスできる変数が欲しい**みたいなときに、
`for`文の初期化部に宣言しちゃうことがあるかもしれません。

上のコードでは、`finished`を「完了した非同期処理の数を表すカウンタ」のつもりで定義しています。
ですが、この性質のせいで**`finished`もクロージャごとに個別な存在になってしまい、`let finished`はいつまで経っても5になりません。**

初期化部で複数の変数を宣言するコードは割と見かけるので、意外とハマる可能性はあるかも？

# 余談

ずっと `for`文で説明してきましたが、`for-in`や`for-of` でも同様の挙動になります。

また `for-in`と`for-of`では、`let`の代わりに`const`を使用することができますが、
この場合もやはり同様の挙動になるようです。

# まとめ

なんだか`let`の性質というより、`for`文の性質になってるような気がしつつ･･･。
`javascript`にもまだまだ知らないこと沢山だなー。