3
4

More than 5 years have passed since last update.

【2019年6月版】jsでクロージャに渡す変数はとりあえずconstしておけッ!!

Last updated at Posted at 2019-06-22

結論

クロージャの中で使いたい変数の宣言に注意しよう。

  • 参照のみの変数は 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 はシングルスレッドで動くかなり特殊な言語で、クロージャが同時に実行されることはないので、一つの変数の読み書き、というレベルで済んでしまいます。お手軽なのはいいですが、副作用は大きいので気をつけてね!というお話でした〜。

参考ページ

クロージャ - JavaScript | MDN

var - JavaScript | MDN

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4