forループのvarをletに変えないとコールバックが同じ値を指す
varはブロックスコープを持たないため、ループ内で生成した関数はすべて同じ変数を共有します。ループが終わったあとに関数を呼び出すと、全て最後の値を参照します。letはループの繰り返しごとに新しい束縛を作るため、この問題が起きません。
なぜエラーが出ないのか
出力: 3 3 3
(エラーなし)- JavaScriptは何も報告しません。文法として正しいためです
出力: 3 3 3- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるか、コードを目で追うしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 function makeGetters() { 2 const getters = []; 3 for (var i = 0; i < 3; i++) { ^ 4 getters.push(function() { return i; }); 5 } 6 return getters; 7 } 8 const getters = makeGetters(); 9 console.log(getters[0](), getters[1](), getters[2]());
出力: 3 3 3期待: 0 1 2 (エラーにはなりません)
varはブロックスコープを持たないため、3つの関数は同じiを共有します。呼び出された時点でのiの値(ループ終了後の3)を全員が参照します。letはループのたびに新しい束縛を作るため、それぞれの関数が自分の回のiを覚えます。
直し方: var を let にします。
広告
広告スロット(未設定)
パターン2
1 function makeMultipliers() { 2 const fns = []; 3 for (var i = 1; i <= 3; i++) { ^ 4 fns.push((x) => x * i); 5 } 6 return fns; 7 } 8 const fns = makeMultipliers(); 9 console.log(fns[0](10), fns[1](10), fns[2](10));
出力: 40 40 40期待: 10 20 30 (エラーにはなりません)
varで宣言したiはループ終了後4になっており、3つの関数すべてがその4を参照してしまいます。letなら各回ごとに独立したiが使われます。
直し方: var を let にします。
パターン3
1 function makeLabelers(labels) { 2 const fns = []; 3 for (var i = 0; i < labels.length; i++) { ^ 4 fns.push(() => labels[i]); 5 } 6 return fns; 7 } 8 const fns = makeLabelers(["a", "b", "c"]); 9 console.log(fns[0](), fns[1](), fns[2]());
出力: undefined undefined undefined期待: a b c (エラーにはなりません)
ループ終了後、varのiは3(配列の範囲外)になっており、labels[3]はundefinedです。letならそれぞれの関数が自分の回のインデックスを覚えます。
直し方: var を let にします。
よくある誤解
「ループの中で作った関数はその時点のiの値を覚えている」という思い込みは、varを使う限り成り立ちません。関数が実際に呼ばれる時点でのiの値(多くの場合ループ終了後の値)を参照します。
まとめ
forループのvarをletに変えないとコールバックが同じ値を指すは中級でつまずきやすい項目です。上の3パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。