概要: このチュートリアルでは、JavaScript クロージャと、コードでクロージャをより効果的に使用する方法について学びます。
JavaScript クロージャの概要
JavaScript では、クロージャとは、内側のスコープから外側のスコープの変数を参照する関数のことです。クロージャは内側のスコープ内に外側のスコープを維持します。
クロージャを理解するには、まず字句スコープがどのように機能するかを知る必要があります。
字句スコープ
字句スコープは、ソースコードで宣言された変数の位置によって変数のスコープを定義します。たとえば、
let name = 'John';
function greeting() {
let message = 'Hi';
console.log(message + ' '+ name);
}
Code language: JavaScript (javascript)
この例では、
- 変数
name
はグローバル変数です。greeting()
関数の内側からでも、どこからでもアクセスできます。 - 変数
message
はローカル変数で、greeting()
関数の内側からしかアクセスできません。
greeting()
関数の外側で message
変数にアクセスしようとすると、エラーが発生します。
そのため、JavaScript エンジンはスコープを使用して変数のアクセシビリティを管理します。
字句スコープによると、スコープはネストさせることができ、内側の関数は外側のスコープで宣言された変数にアクセスできます。たとえば、
function greeting() {
let message = 'Hi';
function sayHi() {
console.log(message);
}
sayHi();
}
greeting();
Code language: JavaScript (javascript)
greeting()
関数は message
という名前のローカル変数と sayHi()
という名前の関数を作成します。
sayHi()
は、greeting()
関数の本体内でのみ使用できる内側の関数です。
sayHi()
関数は、greeting()
関数の message
変数などの外側の関数の変数にアクセスできます。
greeting()
関数の内側で sayHi()
関数を呼び出してメッセージ「Hi」を表示します。
JavaScript クロージャ
greeting()
関数を変更しましょう。
function greeting() {
let message = 'Hi';
function sayHi() {
console.log(message);
}
return sayHi;
}
let hi = greeting();
hi(); // still can access the message variable
Code language: JavaScript (javascript)
今すぐ、greeting()
関数の内側で sayHi()
関数を実行するのではなく、greeting()
関数は sayHi()
関数オブジェクトを返します。
関数は JavaScript ではファーストクラスのオブジェクトなので、別の関数から関数を返すことができることに注意してください。
greeting()
関数の外側で、hi 変数に greeting()
関数によって返された値(sayHi()
関数への参照)を割り当てました。
その後、その関数の参照:hi()
を使用して sayHi()
関数を実行しました。コードを実行すると、上記のときと同じ効果が得られます。
ただし、ここで興味深いのは、通常、ローカル変数は関数が実行されている間のみ存在するということです。
つまり、greeting()
関数の実行が完了すると、message
変数にはアクセスできなくなります。
この場合、sayHi()
関数を参照する hi()
関数を実行すると、message
変数はまだ存在します。
これが魔法であり、クロージャです。言い換えれば、sayHi()
関数はクロージャです。
クロージャは、内側のスコープに外側のスコープを維持する関数です。
さらに多くの JavaScript クロージャの例
次の例は、クロージャのより実用的な例を示しています。
function greeting(message) {
return function(name){
return message + ' ' + name;
}
}
let sayHi = greeting('Hi');
let sayHello = greeting('Hello');
console.log(sayHi('John')); // Hi John
console.log(sayHello('John')); // Hello John
Code language: JavaScript (javascript)
greeting()
関数は、message
という名前の 1 つの引数を取り、name
という名前の単一引数を受け入れる関数返します。
return 関数は、message
変数と name
変数の組み合わせであるグリーティング メッセージを返します。
greeting()
関数は、ファンクション ファクトリーのように動作します。それは、メッセージがそれぞれ Hi
と Hello
の sayHi()
関数と sayHello()
関数を作成します。
sayHi()
と sayHello()
はクロージャです。それらは同じ関数本体を共有しますが、異なるスコープを格納しています。
sayHi()
クロージャでは、message
は Hi
であり、sayHello()
クロージャでは message
は Hello
です。
ループ内の JavaScript クロージャ
次の例を考えてください
for (var index = 1; index <= 3; index++) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
}
Code language: JavaScript (javascript)
出力
after 4 second(s):4
after 4 second(s):4
after 4 second(s):4
Code language: CSS (css)
コードは同じメッセージを表示します。
ループでやりたいことは、イテレーションの時点で、各イテレーションでの i
の値のコピーを作成し、 1、2、3 秒後にメッセージを表示することです。
4 秒後に同じメッセージが表示される理由は、setTimeout()
に渡されたコールバックがクロージャであることです。それらは、4 であるループの最後のイテレーションから i
の値を記憶します。
さらに、for-loop
によって作成された 3 つのクロージャはすべて、同じグローバル スコープを共有し、i
の同じ値にアクセスします。
この問題を解決するには、ループの各イテレーションで新しいクロージャ スコープを作成する必要があります。
一般的な解決策は 2 つあります。IIFE と let
キーワードです。
1) IIFE ソリューションを使用する
このソリューションでは、IIFE(つまりすぐに呼び出される関数表現)を使用します。それは、関数宣言を使用してすぐに実行することで新しいスコープを作成するためです。
for (var index = 1; index <= 3; index++) {
(function (index) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
})(index);
}
Code language: JavaScript (javascript)
出力
after 1 second(s):1
after 2 second(s):2
after 3 second(s):3
Code language: CSS (css)
2) ES6 の let キーワードを使用する
ES6 では、ブロック スコープの変数を宣言するために let
キーワードを使用できます。
for-loop
で let
キーワードを使用すると、各イテレーションで新しいレキシカル スコープが作成されます。つまり、各イテレーションで新しい index
変数を使用できます。
さらに、新しいレキシカル スコープは前のスコープにチェーンされるため、index
の前の値が前のスコープから新しいスコープにコピーされます。
for (let index = 1; index <= 3; index++) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
}
Code language: JavaScript (javascript)
出力
after 1 second(s):1
after 2 second(s):2
after 3 second(s):3
Code language: CSS (css)
まとめ
- レキシカル スコーピングは、JavaScript エンジンがコード内の変数の場所を使用して、その変数が利用できる場所をどのように決定するかを説明します。
- クロージャとは、関数とそのアウター スコープの変数を記憶する能力を組み合わせたものです。