どうも、えんつかです!!
PHPばかり触ってきた身からするとJavaScriptでは想定と違う挙動が起きて、たまに驚きます。
今回の記事は、その中の一つだと思います。完全に「なぜ!?」となり、詰まってしまう原因となったので、記事として残して誰かの解決の糸口になれば嬉しいです。
【JavaScript】ループ内の配列にデータを追加【注意】
早速ですが、皆さんは以下のコードを見て出力はどうなると思いますか??
let hoge = ['a', 'b', 'c'];
let data = {};
let list = [];
for (let i = 0; i < hoge.length; i++) {
let func = (function(){
data.title = hoge[i];
return data;
})( );
list.push(func);
}
console.log(list); //----出力----
もし次のように考えていた方は、大正解です!
[
0: {title: 'c'},
1: {title: 'c'},
2: {title: 'c'},
]
もし、正解された方は流石です!もう以下の記事は読まなくて良いです笑!
もし、仮に次のように考えていた方は、私と同じです笑!
謎現象を一つずつ紐解いていきましょう!
[
0: {title: 'a'},
1: {title: 'b'},
2: {title: 'c'},
]
なぜ、こうならんのや!
hoge~って感じです(頭を抱える)。
本当は、上の様な構成にしたかったのですが、残念でした。しょんぼり。
結論
以下の様にすればOKでした。
let hoge = ['a', 'b', 'c'];
let list = [];
for (let i = 0; i < hoge.length; i++) {
let func = (function(){
let data = {}; // ここを変更
data.title = hoge[i];
return data;
})( );
list.push(func);
}
console.log(list);
変数宣言位置が正しくありませんでした。
let func = (function(){
let data = {};
data.title = hoge[i];
return data;
})( );
list.push(func);
この部分のfunc 変数は即時実行関数と呼ばれ、グローバルスコープの汚染を避ける為に生まれました。
即時実行関数内の変数は、即時実行関数内でのみ参照可能です。
従来のECMAScript5までは、変数宣言にvarしか存在せず、varはどこで宣言しても参照してしまう、「巻き上げ」が発生してしまうので、即時関数を使うことで、参照を限定していました。
この部分で、そもそもfunc変数に即時関数を用いずに、pushすれば良いのではないかと思いますが、そちらも試してて結果的に全部同じ配列となります泣。
原因は、同じく変数宣言の位置です。
ですので、変数宣言の位置がちゃんとfor文内であれば、即時関数がなくても配列に格納されますのでお試しあれ!
for文内にクロージャーを用いたい場合は、即時実行関数に代替することで、ループ内で毎回処理が走ってくれます。
なんでこんなことが起きるのか?
結論としては、「for文内は一つのブロックスコープとして処理されるから」という考えに至りました。
ここは理解度80%位なのですが、デバッグしまくった結果、おそらく以下の様なことが処理として実行されている様です。
- data変数宣言がfor文内だと、data変数にiの初期化条件が流れ、毎度dataにデータ格納され、その結果がfor文外のスコープに代入される。(狙い通り)
- data変数宣言がfor文外だと、data変数に挿入するdata.titleはfor文外のブロックスコープなので、参照はできるものの、最終的に代入するのは、for文が全部処理された後にしかできない。(つまり、配列の最後がまとめて代入されるっぽいです。この部分がキーなんですけどちょっとモヤってます)
よくわかっていないのが、for文外のlist変数宣言直下で、listをデバッグするとすでにデータが格納されているっぽいんですよね…
for文の方が先に処理されるとかあるのかな…
ちょっとモヤッとする終わり方でしたが、おそらく上記の様な形で処理するので、for文を利用する時は気をつけましょう!
もちろん、上記以外の方法で処理させる手法もあると思います!
ので、参考の一つにしてください!
以上!
ほんじゃ〜ね〜