はじめに
es5まではvarでしか変数を宣言できなかったため、ブロック単位で変数を宣言できませんでした。
そのため、以下のようなことが起こります。
for (var i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 1000)
}
/* 結果
(1秒待つ)
10
10
10
10
10
10
10
10
10
10
*/
setTimeoutのラムダ式の中のiはクロージャとしてforのiを読み込みますが、
- setTimeoutにより、1秒後にiの値が読まれる
- iはvarで宣言されているため、forのブロックを抜けても宣言されている
以上の理由から、for文が終わったi = 10の状態で1秒後に一斉に標準出力されるといった感じですね
ところがlet、constが追加され、ブロック単位で変数を宣言できるようになりました。
for文のindexの宣言に関しても、forのブロック単位のスコープになります。
setTimeoutのような非同期だと以下の結果になります。
for (let i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 1000)
}
/* 結果
(1秒待つ)
0
1
2
3
4
5
6
7
8
9
*/
しかしjsに慣れてるとこの挙動に少し違和感を感じたので少し調べてみることにしました。
ただ、jsの実装をみるのもめんどくさいのでbabelとtypescriptでトランスパイルしてみました。
babel
公式サイトでトランスパイルを試せるので、上記のコードを入力してみました。
for (let i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 1000)
}
"use strict";
var _loop = function _loop(i) {
setTimeout(function () {
return console.log(i);
}, 1000);
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
こちらがbabelでのトランスパイルの結果です。
なるほど、関数に値を渡してsetTimeoutを実行しているんですね
もう少し調べてみると、以下のようなfor文にiをクロージャとしてラムダ式に渡すと、関数として展開されるようです。
for (let i = 0; i < 10; i++) {
let func = () => i
console.log(func())
}
"use strict";
var _loop = function _loop(i) {
var func = function func() {
return i;
};
console.log(func());
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
typescript
typescriptでも似たような(というかほぼ同じな)結果が得られました
for (let i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 1000)
}
var _loop_1 = function (i) {
setTimeout(function () { return console.log(i); }, 1000);
};
for (var i = 0; i < 10; i++) {
_loop_1(i);
}
こちらの結果もほぼ変わらずでした。
for (let i = 0; i < 10; i++) {
let func = () => i
console.log(func())
}
var _loop_1 = function (i) {
var func = function () { return i; };
console.log(func());
};
for (var i = 0; i < 10; i++) {
_loop_1(i);
}
あとがき
以前Effective JavaScriptを読んだ時、for文で非同期を実行する際は即時実行関数の中に変数を宣言するやり方が載っていたのですが、その意味がやっとわかった気がします。
jsの仕様は知れば知るほど他の言語と違うし、奥が深くて面白いですね