Code Fix

中級

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 にします。

この問題を解いてみる →

パターン4

 1  function makeCounters() {
 2    const fns = [];
 3    for (var   i = 0; i < 4; i++) {
             ^
 4      fns.push(() => i * i);
 5    }
 6    return fns;
 7  }
 8  const fns = makeCounters();
 9  console.log(fns[0](), fns[1](), fns[2]());
出力: 16 16 16期待: 0 1 4 (エラーにはなりません)

varは3つの関数で同じiを共有するため、全員がループ終了後の値を参照します。

直し方: var を let にします。

この問題を解いてみる →

パターン5

 1  function makeAdders(nums) {
 2    const fns = [];
 3    for (var   i = 0; i < nums.length; i++) {
             ^
 4      fns.push(() => nums[i] + 1);
 5    }
 6    return fns;
 7  }
 8  const fns = makeAdders([5, 10, 15]);
 9  console.log(fns[0](), fns[1](), fns[2]());
出力: NaN NaN NaN期待: 6 11 16 (エラーにはなりません)

配列の要素を参照する場合も同じ罠にかかります。ループ終了後のiは配列の範囲外を指し、nums[i]はundefinedになります。

直し方: var を let にします。

この問題を解いてみる →

よくある誤解

「ループの中で作った関数はその時点のiの値を覚えている」という思い込みは、varを使う限り成り立ちません。関数が実際に呼ばれる時点でのiの値(多くの場合ループ終了後の値)を参照します。

実務での勘所

letを使うとループの繰り返しごとに独立した変数が作られるのは、ES2015の仕様でfor文が特別に扱われているためです。厳密には、for (let i = ...)は繰り返しのたびに新しいiの束縛を作り、前の周回の値を引き継いでコピーする、という仕組みが仕様に明記されています。letが無かった時代は、即時実行関数(IIFE)でループの中身を包み、各周回ごとに新しい関数スコープを作ることでこの問題を回避していました。letの登場で、この技巧的な書き方をしなくても同じ効果が得られるようになりました。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)