概要: このチュートリアルでは、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 variableCode 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 JohnCode 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):4Code 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):3Code 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):3Code language: CSS (css)まとめ
- レキシカル スコーピングは、JavaScript エンジンがコード内の変数の場所を使用して、その変数が利用できる場所をどのように決定するかを説明します。
- クロージャとは、関数とそのアウター スコープの変数を記憶する能力を組み合わせたものです。