JavaScript クロージャ

概要: このチュートリアルでは、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() 関数は、ファンクション ファクトリーのように動作します。それは、メッセージがそれぞれ HiHellosayHi() 関数と sayHello() 関数を作成します。

sayHi()sayHello() はクロージャです。それらは同じ関数本体を共有しますが、異なるスコープを格納しています。

sayHi() クロージャでは、messageHi であり、sayHello() クロージャでは messageHello です。

ループ内の 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-looplet キーワードを使用すると、各イテレーションで新しいレキシカル スコープが作成されます。つまり、各イテレーションで新しい 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 エンジンがコード内の変数の場所を使用して、その変数が利用できる場所をどのように決定するかを説明します。
  • クロージャとは、関数とそのアウター スコープの変数を記憶する能力を組み合わせたものです。
このチュートリアルは役に立ちましたか?