変数のスコープとライフサイクル
JavaScriptでは、varキーワードを使用せずに宣言された変数はグローバルスコープに属します。一方、関数内でvarを使用して宣言された変数はローカル変数として扱われ、関数の外からはアクセスできません。ただし、関数内部からはスコープチェーンを介してグローバルオブジェクトまで変数を検索できます。
var outerFunc = function() {
var localVar = 10;
console.log(localVar); // 10
};
outerFunc();
console.log(localVar); // エラー: localVar is not defined
クロージャの基本と特徴
通常、関数が終了するとそのスコープ内に存在する変数は破棄されますが、クロージャはこのルールに例外です。クロージャは関数とその関数が生成されたときのレキシカルスコープの組み合わせであり、関数の外からでも内部の変数にアクセスし続けることが可能です。
var counterGenerator = function() {
let count = 0;
return function() {
count++;
return count;
};
};
const counterA = counterGenerator();
console.log(counterA()); // 1
console.log(counterA()); // 2
const counterB = counterGenerator();
console.log(counterB()); // 1
console.log(counterB()); // 2
DOMイベントとクロージャの応用
以下の例では、すべてのdiv要素にクリックイベントを割り当てていますが、どの要素をクリックしても同じ値が表示されます。
<div>0</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<script>
const elements = document.getElementsByTagName('div');
for (let index = 0; index < elements.length; index++) {
elements[index].addEventListener('click', function() {
alert(index);
});
}
</script>
問題の原因は、イベントハンドラが非同期で実行されるため、ループが終了した時点のindex値(最終的に5)がすべてのイベントハンドラで参照されることです。これを解決するには、クロージャを使って各ループの値を保持します。
<script>
const elements = document.getElementsByTagName('div');
for (let index = 0; index < elements.length; index++) {
elements[index].addEventListener('click', (function(value) {
return function() {
alert(value);
};
})(index));
}
</script>
配列へのクロージャの代入
次の例では、関数が配列に複数の関数を格納して返しますが、すべての関数が最終的な値を返してしまいます。
function createCounterList() {
const list = [];
for (let i = 0; i < 10; i++) {
list[i] = function() {
return i;
};
}
return list;
}
for (let j = 0; j < 10; j++) {
console.log(createCounterList()[j]()); // 全て10
}
これはクロージャが変数iへの参照を保持しているためです。解決策として、即時関数を使って現在の値を固定します。
function createCounterList() {
const list = [];
for (let i = 0; i < 10; i++) {
list[i] = (function(currentValue) {
return function() {
return currentValue;
};
})(i);
}
return list;
}
for (let j = 0; j < 10; j++) {
console.log(createCounterList()[j]()); // 0から9まで順に
}
クロージャの利点と注意点
クロージャは外部関数の変数にアクセスできるため、関数の内部状態を外部に公開する手段として有効です。しかし、メモリ消費が大きいため、不必要なクロージャの生成は避けるべきです。
function createPerson() {
let name = 'Alice';
let id = 16;
return {
getInfo: function() {
return `${name}: ${id}`;
}
};
}
const person = createPerson();
console.log(person.getInfo()); // Alice: 16
メモリリークの防止
クロージャ内にDOM要素を保持すると、メモリリークの原因になることがあります。この問題を回避するには、不要な参照を明示的に解除します。
function assignClickHandler() {
const element = document.getElementById('target');
const id = element.id;
element.addEventListener('click', function() {
console.log(id);
});
element = null; // DOM参照を明示的に解除
}
ブロックスコープの模倣
JavaScriptにはブロックスコープが存在しないため、関数スコープを利用してブロックスコープを模倣できます。
(function() {
for (var i = 0; i < 5; i++) {
console.log(i); // 0, 1, 2, 3, 4
}
})();
console.log(i); // エラー: i is not defined