ループ内クロージャー解説
JavaScriptのループ内クロージャー:シンプルな実用例
JavaScriptにおけるループ内クロージャーは、ループ内で定義された関数がループ外でもそのループのスコープを保持する現象です。これは、関数が作成された時点での変数の値を保持するからです。
簡単な例
function createFunctions() {
const functions = [];
for (let i = 0; i < 5; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const myFunctions = createFunctions();
myFunctions[0](); // 0
myFunctions[1](); // 1
myFunctions[2](); // 2
myFunctions[3](); // 3
myFunctions[4](); // 4
解説
createFunctions
関数内で、空の配列functions
を作成します。- ループ内で、匿名関数を
functions
配列に追加します。この関数は、console.log(i)
を実行します。 - ループが終了した後、
myFunctions
変数にcreateFunctions
関数の戻り値(functions
配列)を代入します。 myFunctions
配列内の関数を呼び出すと、それぞれが正しい値を出力します。これは、各関数が作成された時点でのi
の値を保持しているためです。
ポイント
- これは、クロージャーと呼ばれる現象です。クロージャーは、関数がその外部の変数を参照できることを意味します。
- しかし、ループ内で定義された関数は、その関数が作成された時点でのスコープを保持します。そのため、ループが終了した後でも、その関数は元の
i
の値を参照できます。 - JavaScriptの変数スコープはブロックスコープです。つまり、
for
ループ内の変数i
は、そのブロック内でのみ有効です。
注意
- 現在では、
let
またはconst
キーワードを使用して変数を宣言することを推奨します。これにより、ブロックスコープが確保され、クロージャーの挙動がより予測可能になります。 - 以前は、
var
キーワードを使用して変数を宣言すると、関数スコープになり、ループ内の変数がループ外でも有効になっていました。しかし、これは多くの場合、意図しない結果をもたらしました。
ループ内クロージャーとは?
JavaScriptのクロージャーは、関数がその定義されたスコープ(レキシカル環境)を覚えており、そのスコープ内の変数にアクセスできるという特徴を持つ仕組みです。ループ内でクロージャーを使うと、各反復で異なる値を持つ関数を生成することができます。
例1:シンプルなカウンター
function createCounters() {
const counters = [];
for (let i = 0; i < 3; i++) {
counters.push(function() {
return i;
});
}
return counters;
}
const myCounters = createCounters();
console.log(myCounters[0]()); // 0
console.log(myCounters[1]()); // 1
console.log(myCounters[2]()); // 2
- 解説
for
ループで、i
の値が異なる関数をcounters
配列に格納します。- 各関数は、その作成時の
i
の値を返すように定義されています。 - ループが終了した後、
myCounters
配列の各要素を呼び出すと、それぞれが異なる値を返します。これは、各関数が作成された時点でのi
の値を保持しているためです。
例2:イベントリスナー
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('You clicked button number ' + (i + 1));
});
}
- 解説
- 各ボタンにクリックイベントリスナーを追加します。
- イベントハンドラー関数内で、
i
の値を使ってクリックされたボタンの番号を表示します。 let
を使用することで、各イベントハンドラーが異なるi
の値を参照できるようになっています。
なぜlet
を使うのか?
- varとの違い
var
を使うと、変数が関数スコープになるため、全てのイベントハンドラーが同じi
を参照してしまい、最後の値しか表示されません。let
を使うと、ブロックスコープになるため、各繰り返しで新しいi
が作成され、各イベントハンドラーが異なる値を参照できます。
- 代替案
- 複雑性
- パフォーマンス
ループ内クロージャーは、JavaScriptの強力な機能ですが、使い方を誤ると予期せぬ結果になることがあります。let
キーワードを正しく使い、クロージャーの仕組みを理解することで、より柔軟なJavaScriptプログラミングが可能になります。
IIFE(Immediately Invoked Function Expression)
を使って、より厳密なスコープ管理を行うこともできます。- クロージャーは、イベントリスナーだけでなく、タイマー関数や非同期処理など、さまざまな場面で活用できます。
より詳しく知りたい場合は、以下のキーワードで検索してみてください。
- IIFE
- let const var
- JavaScript ループ
forEachメソッドを使う
forEach
メソッドは、配列の各要素に対して関数を呼び出すための高階関数です。このメソッドを使うことで、ループを明示的に書く必要がなくなり、コードが簡潔になります。
const buttons = document.querySelectorAll('button');
buttons.forEach((button, index) => {
button.addEventListener('click', () => {
console.log('You clicked button number ' + (index + 1));
});
});
この例では、forEach
メソッドを使って各ボタンにイベントリスナーを追加しています。index
パラメータを使ってボタンのインデックスを取得し、クロージャーでその値を保持しています。
mapメソッドを使う
map
メソッドは、配列の各要素に対して関数を適用し、その結果を新しい配列として返す高階関数です。新しい配列の要素に、クロージャーで生成した関数を格納することができます。
const createCounters = () => {
return Array.from({ length: 3 }, (_, i) => () => i);
};
const myCounters = createCounters();
この例では、map
メソッドを使って、i
の値が異なる関数の配列を作成しています。
for...ofループを使う
for...of
ループは、配列や文字列などのイテラブルなオブジェクトの要素を順に処理するためのループ構文です。let
キーワードを使うことで、各反復で新しいスコープが作成され、クロージャーの問題を回避できます。
const buttons = document.querySelectorAll('button');
for (const button of buttons) {
button.addEventListener('click', () => {
// ...
});
}
IIFE(Immediately Invoked Function Expression)を使う
IIFEは、関数を定義と同時に実行するテクニックです。IIFEを使うことで、新しいスコープを作成し、変数のスコープを制限することができます。
for (let i = 0; i < 3; i++) {
(function(index) {
const counter = () => index;
// ...
})(i);
}
どの方法を選ぶべきか?
- 柔軟性
IIFEは、より細かいスコープ管理が必要な場合に有効です。 - 可読性
for...of
ループは、より自然なループの書き方です。 - 簡潔さ
forEach
やmap
メソッドは、コードを簡潔に記述できます。
javascript loops closures