結論
クロージャの中で使いたい変数の宣言に注意しよう。
- 参照のみの変数は
const
宣言マスト。でもあとは気にしなくてOK。 - 書き込みしたい変数は
let
宣言して腹をくくる。 -
var
は使わない。
時々、忘れてしまうのでメモです。
クロージャ内から使用される変数に刮目せよ
js ではクロージャをバンバン使いますが、クロージャ内で使用する変数には適切な宣言をしておかないとバグを埋め込む原因となります。
例えば以下のようなコードで、
// DB や APIなどから非同期で処理〜
function getPromise(c) {
return new Promise((accept)=>accept(c));
}
// 何かのマスターを元にループで処理したい
const array = [1,2,3,4,5];
for(a of array) {
msg = `[${a}] <- イマココ`;
getPromise(a).then(v => {
console.log(`値: ${v}`);
console.log(`処理中: ${msg}`);
})
msg = `[${a}] <- 終了!お疲れ様でした。`
console.log(msg);
}
getPromise(a).then(v => {...}
で変数 a
の値が変数 v
に渡されてきますが、この v => {...}
の中で参照されている変数 msg
の宣言はどうなっているでしょうか?
msg = '[${a}] <- イマココ'
で宣言無しにいきなり変数定義してしまっています。熟練の方は一瞬でをいをいおと気づいていただけるでしょうが、例えば僕は数千行に渡るデータコンバート用の単発のバッチを涙ながらにjsでガリガリ書いているわけです。
DB A
の テーブル B
からデータを読み込み、 DB C
の テーブル D
のマスターを参照しつつデータを変換して DB E
の テーブル F
へデータを書き込んでいくわけです。いちいち変数宣言してられるかーーー!!
・・・とりあえず区切りのいいところで試しに動かして見るか、とテスト環境を対象に上記のバッチを流してみると・・・
$ npm start
[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。
値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。
処理中:
のメッセージで イマココ
を確認したかったのに、終了
メッセージが表示されています。ウホー!
最初の5行の出力はちょっと目障りなので出力をやめてみましょう。
function getPromise(c) {
return new Promise((accept)=>accept(c));
}
const array = [1,2,3,4,5];
for(a of array) {
msg = `[${a}] <- イマココ`;
getPromise(a).then(v => {
console.log(`値: ${v}`);
console.log(`処理中: ${msg}`);
});
msg = `[${a}] <- 終了!お疲れ様でした。`;
}
これを実行してみます。
$ npm start
> node index.js
値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。
終了メッセージに書き換わっている・・・。
いやいや、getPromise(a)
の後で msg = `[${a}] <- 終了!お疲れ様でした。`;
とmsgの代入処理をしているのでこの問題点はすぐに気づけるはずです。俺としたことが・・・ちょっと疲れたのかな。。。
それではこれではどうでしょう?
function getPromise(c) {
return new Promise((accept)=>accept(c));
}
const array = [1,2,3,4,5];
for(a of array) {
msg = `[${a}] <- イマココ`;
getPromise(a).then(v => {
console.log(`値: ${v}`);
console.log(`処理中: ${msg}`);
});
}
getPromise(a)
の後で msg
には何も入れていません。というか msg
は宣言してからクロージャの中でしか使用していません。よし、万全。指差し確認、OK!
では、バッチを動かしてみましょう。
$ npm start
> node index.js
値: 1
処理中: [5] <- イマココ
値: 2
処理中: [5] <- イマココ
値: 3
処理中: [5] <- イマココ
値: 4
処理中: [5] <- イマココ
値: 5
処理中: [5] <- イマココ
イマココ
メッセージが全部 5
になっているーーー!!!
これはメッセージに出力しているだけなので、msg
が変わってしまっていても別にフーンで済むと思うかもしれません。
では、この msg
変数がDBの書き込み先だったり、APIのエンドポイントだったと想定したらどうでしょう?
すべてのデータが5番の接続先へ流れていきます。APIやRDBMSなら引数エラーや最悪でもスキーマの不一致などでフェイタルエラーで止まってくれるかもしれませんが、MongoDBやKVSなどのスキーマレスのNoSQLやただのフォルダ、アップロードするだけなどの場合は普通に処理が完了してしまうでしょう。
もう、白目、ですね・・・。想像しただけで胃が縮む思いです。
どうしてこうなったのか・・・? 理解するのには最初の冗長なメッセージを出していたバージョンのプログラムをもう一度、見るのがわかりやすいのです。
function getPromise(c) {
return new Promise((accept)=>accept(c));
}
for(a of array) {
msg = `[${a}] <- イマココ`;
getPromise(a).then(v => {
console.log(`値: ${v}`);
console.log(`処理中: ${msg}`);
});
msg = `[${a}] <- 終了!お疲れ様でした。`;
console.log(msg);
}
そして実行結果は、
$ npm start
[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。
値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。
となります。
forループの最後で宣言した console.log(msg);
の結果である、
[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。
がすべて先に表示されています。
つまり、先にfor ループ が完了している ということです。 Promise
で処理を後回しにしたので、すべての for ループが完了してから、Promise
の (accept)=>accept(c)
が呼び出され、v => {...}
が呼び出されます。
・・・for
ループが完了しているのに、for ループ内で宣言した変数msg
はどうなっているのか・・・?
これがクロージャがクロージャたる所以なのですが、v => {...}
が宣言された時点でのすべての変数がレキシカル環境として記憶されています。要はどこかで変数 msg
の存在を覚えててくれるわけですが、この変数が書き込み有りか無しかで持ち方が変わってしまう、というのがキモなワケです。
今回は手抜きをして変数宣言をすっ飛ばした結果、デフォルトの宣言である書き込み可能な var
として定義されてしまいました。
よってクロージャの中では書き込み用の msg
というみんなで読み書きできる1つの場所
として変数がmsg
が使用されています。その結果、forの最後のループで書き込まれた5
がすべてのクロージャで参照された、ということになります。
これはクロージャの中から値を取り出したい場合のような明確な書き込み用途
の変数の場合は当然の結果です。
というわけで、書き込み用途ではない場合には明確に読み取り専用変数である**const
の宣言がマスト**なのです。
ここではmsg
は変わって欲しくない変数のため、以下のように const
宣言をちゃんとします。
function getPromise(c) {
return new Promise((accept)=>accept(c));
}
const array = [1,2,3,4,5];
for(a of array) {
const msg = `[${a}] <- イマココ`;
getPromise(a).then(v => {
console.log(`値: ${v}`);
console.log(`処理中: ${msg}`);
});
}
そして、実行してみると・・・
$ npm start
> node index.js
値: 1
処理中: [1] <- イマココ
値: 2
処理中: [2] <- イマココ
値: 3
処理中: [3] <- イマココ
値: 4
処理中: [4] <- イマココ
値: 5
処理中: [5] <- イマココ
const
宣言をして、宣言した時点の値をちゃんとキャプチャさせた結果、ちゃんと 1〜5 のイマココ
メッセージが表示されました。やれやれだぜ。。。
var
はやばい。
さらに今回は変数の宣言をせずにデフォルトの var
が使われてしまいましたが、var
は関数スコープでfunction
オブジェクトのプロパティ
のように振る舞うので、副作用が大きいです。
function majisuka() {
var x = 'マジで?'
let y = '嘘でしょ?'
{
var x = 'マジすか?'
let y = '本当に?'
}
console.log(`x is [${x}]`);
console.log(`y is [${y}]`);
}
majisuka();
こんな適当なプログラムでも、ちゃんと結果を予測できますか?結果は以下のようになります。
x is [マジすか?]
y is [嘘でしょ?]
var
で宣言したx
はブロック内で宣言したx
で見事に上書きされています。マジすか?!
そんなわけでクロージャ内で書き込みたい変数にはちゃんと let
宣言をしましょう!!
ちなみに java では
String msg = "こんにちわ"; // final 宣言していない
new Thread(() -> {
msg = "こんばんわ"; // -> これは"コンパイルエラー"
System.out.println(msg);
});
などの、クロージャ外で宣言された変数へのアクセスはコンパイルエラーとなります。final
宣言が Must
で読み取り専用の変数しか渡せません。
例えば上記のように本当にマルチスレッドで動かすためのクロージャでは、クロージャの外部へ値を書き出すことはそのままスレッド間通信
となるので、変数への書き込みのようなお手軽な読み書きじゃダメなのですね。
このように場合に応じてロックのストラテジを各自でちゃんと考えて用意してね、というわけなのです。
js はシングルスレッドで動くかなり特殊な言語で、クロージャが同時に実行されることはないので、一つの変数の読み書き、というレベルで済んでしまいます。お手軽なのはいいですが、副作用は大きいので気をつけてね!というお話でした〜。