A Simple Explanation of JavaScript Closures

コールバック、イベントハンドラ、高次関数は、クロージャのおかげで外部スコープの変数にアクセスできます。 クロージャは関数型プログラミングにおいて重要であり、JavaScriptのコーディング面接でもよく聞かれます。

あらゆるところで使われている一方で、クロージャは把握するのが難しい。 もしあなたがクロージャを理解する上で「あっ!」と思う瞬間がないのであれば、この記事はあなたのためのものです。

まず、基本的な用語であるスコープとレキシカルスコープについて説明します。 そして、基本を理解した後、最終的にクロージャを理解するためには、たった1つのステップが必要になります。

始める前に、スコープとレキシカル スコープのセクションをスキップしたいという衝動を抑えることをお勧めします。 これらの概念はクロージャにとって非常に重要であり、これらをしっかりと理解すれば、クロージャのアイデアは自明のものとなります。

1 スコープ

変数を定義するとき、ある境界内でアクセスできるようにしたいと思います。 たとえば、resultcalculate()calculate()resultの変数は役に立たないのです。

変数のアクセス性はスコープで管理されています。 そのスコープ内で定義された変数には自由にアクセスできます。 しかし、そのスコープの外では、その変数にはアクセスできません。

JavaScriptでは、関数やコードブロックによってスコープが作られます。

スコープが変数の利用可能性にどのように影響するかを見てみましょう countfoo() によって作成されたスコープに属しています:

function foo() { // The function scope let count = 0; console.log(count); // logs 0}foo();console.log(count); // ReferenceError: count is not defined

countfoo() のスコープ内では自由にアクセスできます。

しかし、foo()countcountにアクセスしようとすると、JavaScriptはReferenceError: count is not definedを投げます。

JavaScriptのスコープでは、「関数やコードブロックの中で変数を定義した場合、その変数はその関数やコードブロックの中でのみ使用できる」とされています。 上の例では、この動作を示しています。

JavaScriptのスコープ

さて、一般的な定式化を見てみましょう。

スコープは、変数のアクセス性を規定する空間ポリシーです。

すぐにわかる性質として、スコープは変数を分離します。 異なるスコープでも同じ名前の変数を持つことができるので、これは素晴らしいことです。

共通の変数名(countindexcurrentvalueなど)を、異なるスコープで衝突することなく再利用することができます。

foo()bar()countがあります。

function foo() { // "foo" function scope let count = 0; console.log(count); // logs 0}function bar() { // "bar" function scope let count = 1; console.log(count); // logs 1}foo();bar();

countfoo()bar() の関数スコープの変数は衝突しません。

2. スコープの入れ子

もう少しスコープを使って、あるスコープを別のスコープに入れて遊んでみましょう。

関数 innerFunc()outerFunc() の中に入れ子になっています。

JavaScriptのスコープは入れ子にすることができます

2つの関数のスコープはどのように相互作用するでしょうか?

outerFunc()outerVarinnerFunc()のスコープ内からアクセスすることはできますか?

例題で試してみましょう:

function outerFunc() { // the outer scope let outerVar = 'I am outside!'; function innerFunc() { // the inner scope console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();

確かに、outerVarinnerFunc()のスコープ内でアクセス可能です。 外側のスコープの変数は内側のスコープの中でアクセスできます。

これで2つの面白いことがわかりました:

  • スコープは入れ子にすることができます
  • 外側のスコープの変数は内側のスコープの中でアクセスできます

3. レキシカルスコープ

JavaScriptは、innerFunc()outerVarouterFunc()outerVarに対応していることをどのように理解しているのでしょうか。

それは、JavaScriptがレキシカル・スコープ(またはスタティック・スコープ)と呼ばれるスコープの仕組みを実装しているからです。 レキシカル・スコープとは、変数のアクセス性が、入れ子になったスコープ内のソースコードにおける変数の位置によって決まるというものです。

簡単に言うと、レキシカル スコーピングとは、内側のスコープの中で、その外側のスコープの変数にアクセスできることを意味します。

レキシカル (またはスタティック) と呼ばれるのは、エンジンが (レキシング時に) スコープの入れ子を、実行せずに JavaScript のソース コードを見ただけで判断するからです。

以下は、エンジンが前のコード スニペットをどのように理解するかを示しています。

  1. 変数 outerVarouterFunc() を定義しているのがわかります。 いいですね。
  2. outerFunc()innerFunc()という関数を定義しているのがわかります。
  3. innerFunc()outerVarinnerFunc()outerVarouterFunc()outerVarと同じ変数であると考えます。

レキシカルスコープの抽出されたアイデア:

レキシカルスコープは、静的に決定された外側のスコープで構成されます。

例えばです。

const myGlobal = 0;function func() { const myVar = 1; console.log(myGlobal); // logs "0" function innerOfFunc() { const myInnerVar = 2; console.log(myVar, myGlobal); // logs "1 0" function innerOfInnerOfFunc() { console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0" } innerOfInnerOfFunc(); } innerOfFunc();}func();

innerOfInnerOfFunc()innerOfFunc()func()innerOfInnerOfFunc()myInnerVarmyVarmyGlobalにアクセスできます。

innerFunc()func()innerOfFunc()myVarmyGlobalにアクセスできます。

最後に、func()func()myGlobalにアクセスできます。

4. クロージャ

よし、レキシカル スコープで外側のスコープの変数に静的にアクセスできるようになりました。 クロージャまであと一歩ですね。

もう一度、outerFunc()innerFunc()の例を見てみましょう。

function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();

innerFunc()outerVarがレキシカルスコープからアクセスされています。 これはすでに知られています。

innerFunc()outerFunc()のスコープ)の中で行われることに注意してください。

変更してみましょう。 innerFunc()outerFunc()innerFunc()outerVarにアクセスすることができるでしょうか?

コード スニペットを調整しましょう:

function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } return innerFunc;}const myInnerFunc = outerFunc();myInnerFunc();

現在、innerFunc()はその語彙的なスコープの外で実行されています。 そして重要なのは

innerFunc()outerVarへのアクセスが可能です。

つまり、innerFunc()outerVarをそのレキシカルスコープからクローズオーバー(捕捉、記憶)しているのです。

つまり、innerFunc()outerVarをその辞書的スコープから閉じているので、クロージャです。

JavaScriptのクロージャ

クロージャとは何かを理解するための最後の一歩を踏み出しました:

クロージャは、その語彙的スコープの外で実行されても、その語彙的スコープにアクセスする関数です。

もっと簡単に言うと、クロージャは、後でどこで実行されるかに関わらず、定義された場所の変数を記憶する関数です。

クロージャを識別するための経験則ですが、関数内に外部変数(関数内で定義されていない)がある場合、外部変数が取り込まれているため、その関数はクロージャである可能性が高いです。

先ほどのコードでは、outerVarouterFunc()innerFunc()の中にあるエイリアン変数です。

続いて、なぜクロージャーが便利なのかを示す例を見てみましょう。

5. クロージャの例

5.1 イベント ハンドラ

ボタンが何回クリックされたかを表示してみましょう:

let countClicked = 0;myButton.addEventListener('click', function handleClick() { countClicked++; myText.innerText = `You clicked ${countClicked} times`;});

デモを開き、ボタンをクリックします。 テキストが更新され、クリックされた回数が表示されます。

ボタンがクリックされると、DOMコードのどこかでhandleClick()が実行されます。 その実行は、定義された場所から遠く離れたところで行われます。

しかし、クロージャであるため、handleClick()countClickedmyTextもキャプチャされます。

5.2 コールバック

レキシカル スコープから変数をキャプチャすることは、コールバックで役に立ちます。

setTimeout() コールバック:

const message = 'Hello, World!';setTimeout(function callback() { console.log(message); // logs "Hello, World!"}, 1000);

callback()message を捕捉しているので、クロージャです。

forEach()のイテレータ関数:

let countEven = 0;const items = ;items.forEach(function iterator(number) { if (number % 2 === 0) { countEven++; }});countEven; // => 2

iteratorcountEvenという変数を捕らえているのでクロージャーとなります。

5.3 関数型プログラミング

引数が完全に供給されるまで、関数が別の関数を返すとき、カリー化が起こります。

例えば、

function multiply(a) { return function executeMultiply(b) { return a * b; }}const double = multiply(2);double(3); // => 6double(5); // => 10const triple = multiply(3);triple(4); // => 12

multiply は、別の関数を返すcurrying関数です。

関数型プログラミングの重要な概念であるcurryingも、クロージャのおかげで可能になりました。

executeMultiply(b)aaba * bが計算されます。

6. 結論

JavaScriptの変数のアクセス性を規定しているのはスコープです。 スコープには、関数やブロックのスコープがあります。

レキシカルスコープにより、関数スコープは外側のスコープから変数に静的にアクセスすることができます。

最後に、クロージャは、そのレキシカルスコープから変数を取り込む関数です。 簡単に言えば、クロージャはどこで実行されても、それが定義されている場所からの変数を記憶します。

クロージャは、イベントハンドラやコールバックの中で変数を捕捉します。 関数型プログラミングで使われています。

Frontendの面接では、クロージャの仕組みを聞かれることもあります。

すべてのJavaScript開発者はクロージャがどのように動作するかを知っておく必要があります。 対処してください⌐■_■.

チャレンジしてみませんか? JavaScriptクロージャーに関する7つの面接質問。 あなたはそれらに答えることができますか?

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です