概要
「Effective JavaScript」と「JavaScript Ninja の極意」という書籍でぼちぼち JavaScript の修行をしております。
その修業の中で苦労ジャ初心者の僕がクロウしたポイントを紹介したいと思います。
上忍のみなさんにとってはキホンのキかもしれませんが…。
問題
まず、以下のコードを実行します。
var myFuctions = [], girls = ['Yuno', 'Miyako', 'Matsuri'], i;
for (i = 0; i < girls.length; i++) {
myFuctions[i] = function () {
console.log('My name is ' + girls[i] + '.');
};
}
その後、以下のコードを実行するとコンソールにどのような文字列が表示されるでしょう。
myFuctions[0](); // ?
myFuctions[1](); // ?
myFuctions[2](); // ?
僕の予想 : こうとしか考えられませんぞwww よゆうwwwwww
myFuctions[0](); // My name is Yuno.
myFuctions[1](); // My name is Miyako.
myFuctions[2](); // My name is Matsuri.
現実
myFuctions[0](); // My name is undefined.
myFuctions[1](); // My name is undefined.
myFuctions[2](); // My name is undefined.
ゆのっち達はどこに消えたんでしょうか…。
原因
実はそれぞれの関数の内容は以下のようになっていたのです。
var myFuctions = [], girls = ['Yuno', 'Miyako', 'Matsuri'], i;
for (i = 0; i < girls.length; i++) {
myFuctions[i] = function () {
console.log('(i: ' + i + ') My name is ' + girls[i] + '.');
};
}
myFuctions[0](); // (i: 3) My name is undefined.
myFuctions[1](); // (i: 3) My name is undefined.
myFuctions[2](); // (i: 3) My name is undefined.
そう、どの関数でも i = 3、つまり for 文が終了した時の i の値となっていたのでした。
(i が 3 にインクリメントされた後に for ループが終了)
これは
クロージャは外側の変数を値ではなくリファレンスによって保存する
という特性によって生じます。
解決方法
これを期待通りの挙動に修正するには即時関数を用いてローカルスコープを生成させます。
var myFuctions = [], girls = ['Yuno', 'Miyako', 'Matsuri'], i;
for (i = 0; i < girls.length; i++) {
(function (j) {
myFuctions[j] = function () {
console.log('(i: ' + i + ', j: ' + j + ') My name is ' + girls[j] + '.');
};
})(i);
}
こうすれば for ループの各ステップのそれぞれのローカルスコープ内で j という変数が新たに定義され、
そこに値が保持されます。
ぎゅぎゅっと瞬間凍結
myFuctions[0](); // (i: 3, j: 0) My name is Yuno.
myFuctions[1](); // (i: 3, j: 1) My name is Miyako.
myFuctions[2](); // (i: 3, j: 2) My name is Matsuri.
✌ ('ω' ✌ )三 ✌ ('ω') ✌ 三( ✌ 'ω') ✌ うごいた!
この話、JavaScript Ninja 等を読むまではイメージでなんとなく掴んでたんですが、しっかり原理を把握しておかないと、忘れた頃に躓いてしまいますね…。
追記
ECMAScript 6 では let が使えるようになり、ブロックスコープが設定できるみたいですね。