こうこく
作 ▸
改 ▸

非同期のコールバック関数に外から値を渡す

コールバック関数のパラメータ以外のものを外から渡す方法について。

ここでは setTimeout() の第1引数に渡すコールバック関数を例にします。

単に主題の件をやりたければ、コールバック関数のスコープ外で宣言した変数に渡したい値をセットしておいて、それをコールバック関数の内側から参照させればいいです。

例として次のコードを実行すると、1000ミリ秒後に alert で『(^-^)』が表示されます。この場合、str がコールバック関数のスコープ外の変数にあたります。

const str = '(^-^)';

setTimeout(() => {
  alert(str);
}, 1000);

ただしこの方法で参照される str の値は、実際にコールバック関数が実行された時点における str の値となります。

それを確認できるのが次の例です。次のコードを実行すると、1000ミリ秒後に alert() で表示されるのは『(^-^)』ではなく『(>_<)』です。

let str = '(^-^)';

setTimeout(() => {
  alert(str); // 実際にこれが実行される時点での str は '(>_<)'
}, 1000);

str = '(>_<)';

setTimeout() はnミリ秒後に行う処理を予約する関数です。setTimeout()alert() を予約した後、1000ミリ秒後までに str の中身が変化してるので、このようになります。

もし「コールバック関数を仕掛けたタイミングにおける変数の値」を実行時にも使用したい場合は、次のように書きます。次のコードを実行すると、1000ミリ秒後も alert で『(^-^)』が表示されます。

let str = '(^-^)';
setTimeout(createCallback(str), 1000);
str = '(>_<)';

function createCallback(arg) {
  return () => {
    alert(arg);
  }
}

これは、「『alert() を行う無名関数』を生成して返却する別の関数 (ここでは createCallback())」を間に挟んでる形です。

JavaScriptの引数は、Object型ではない限り値渡し = 『値がコピーされた別の変数』扱いです。createCallback() で生成した無名関数が参照している arg は、あくまで createCallback() が実行されたときに渡された str のコピーなので、こういうことができます。

余談

なんでこんな記事を書いたのかというと、初心者の頃にループ内で非同期のコールバック関数に外からループカウンタを渡して、想定通りに動かなかったことがあるからです。

例として次のコードを実行すると、1000ミリ秒後に alert()3 が3回表示されます。実際に setTimeout() で仕掛けたコールバックが実行されるタイミングでは、すでに i の値がループを走り抜けて 3 に変化しているためです。

※想定通りに動かなかったコード
// 変数宣言が var なのは当時の再現です
for (var i = 0; i < 3; ++i) {
  setTimeout(() => {
    alert(i); // 実行時には i = 3
  }, 1000);
}

なお、このケースに限っては var の代わりに let を使うだけで解決します。let ならばループごとに別スコープ扱いになるので、同じように書いても1000ミリ秒後に alert()0 -> 1 -> 2 と表示されます。

let使えばいい
for (let i = 0; i < 3; ++i) {
  setTimeout(() => {
    alert(i); // 実行時には i = 0 -> 1 -> 2
  }, 1000);
}
この記事に何かあればこちらまで (非公開)