コールバック、イベントハンドラ、高次関数は、クロージャのおかげで外部スコープの変数にアクセスできます。 クロージャは関数型プログラミングにおいて重要であり、JavaScriptのコーディング面接でもよく聞かれます。
あらゆるところで使われている一方で、クロージャは把握するのが難しい。 もしあなたがクロージャを理解する上で「あっ!」と思う瞬間がないのであれば、この記事はあなたのためのものです。
まず、基本的な用語であるスコープとレキシカルスコープについて説明します。 そして、基本を理解した後、最終的にクロージャを理解するためには、たった1つのステップが必要になります。
始める前に、スコープとレキシカル スコープのセクションをスキップしたいという衝動を抑えることをお勧めします。 これらの概念はクロージャにとって非常に重要であり、これらをしっかりと理解すれば、クロージャのアイデアは自明のものとなります。
1 スコープ
変数を定義するとき、ある境界内でアクセスできるようにしたいと思います。 たとえば、result
calculate()
calculate()
result
の変数は役に立たないのです。
変数のアクセス性はスコープで管理されています。 そのスコープ内で定義された変数には自由にアクセスできます。 しかし、そのスコープの外では、その変数にはアクセスできません。
JavaScriptでは、関数やコードブロックによってスコープが作られます。
スコープが変数の利用可能性にどのように影響するかを見てみましょう count
foo()
によって作成されたスコープに属しています:
function foo() { // The function scope let count = 0; console.log(count); // logs 0}foo();console.log(count); // ReferenceError: count is not defined
count
foo()
のスコープ内では自由にアクセスできます。
しかし、foo()
count
count
にアクセスしようとすると、JavaScriptはReferenceError: count is not defined
を投げます。
JavaScriptのスコープでは、「関数やコードブロックの中で変数を定義した場合、その変数はその関数やコードブロックの中でのみ使用できる」とされています。 上の例では、この動作を示しています。
さて、一般的な定式化を見てみましょう。
スコープは、変数のアクセス性を規定する空間ポリシーです。
すぐにわかる性質として、スコープは変数を分離します。 異なるスコープでも同じ名前の変数を持つことができるので、これは素晴らしいことです。
共通の変数名(count
index
current
value
など)を、異なるスコープで衝突することなく再利用することができます。
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();
count
foo()
bar()
の関数スコープの変数は衝突しません。
2. スコープの入れ子
もう少しスコープを使って、あるスコープを別のスコープに入れて遊んでみましょう。
関数 innerFunc()
outerFunc()
の中に入れ子になっています。
2つの関数のスコープはどのように相互作用するでしょうか?
outerFunc()
outerVar
innerFunc()
のスコープ内からアクセスすることはできますか?
例題で試してみましょう:
function outerFunc() { // the outer scope let outerVar = 'I am outside!'; function innerFunc() { // the inner scope console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
確かに、outerVar
innerFunc()
のスコープ内でアクセス可能です。 外側のスコープの変数は内側のスコープの中でアクセスできます。
これで2つの面白いことがわかりました:
- スコープは入れ子にすることができます
- 外側のスコープの変数は内側のスコープの中でアクセスできます
3. レキシカルスコープ
JavaScriptは、innerFunc()
outerVar
outerFunc()
outerVar
に対応していることをどのように理解しているのでしょうか。
それは、JavaScriptがレキシカル・スコープ(またはスタティック・スコープ)と呼ばれるスコープの仕組みを実装しているからです。 レキシカル・スコープとは、変数のアクセス性が、入れ子になったスコープ内のソースコードにおける変数の位置によって決まるというものです。
簡単に言うと、レキシカル スコーピングとは、内側のスコープの中で、その外側のスコープの変数にアクセスできることを意味します。
レキシカル (またはスタティック) と呼ばれるのは、エンジンが (レキシング時に) スコープの入れ子を、実行せずに JavaScript のソース コードを見ただけで判断するからです。
以下は、エンジンが前のコード スニペットをどのように理解するかを示しています。
- 変数
outerVar
outerFunc()
を定義しているのがわかります。 いいですね。 -
outerFunc()
innerFunc()
という関数を定義しているのがわかります。 -
innerFunc()
outerVar
innerFunc()
outerVar
outerFunc()
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()
myInnerVar
myVar
myGlobal
にアクセスできます。
innerFunc()
func()
innerOfFunc()
myVar
myGlobal
にアクセスできます。
最後に、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
をその辞書的スコープから閉じているので、クロージャです。
クロージャとは何かを理解するための最後の一歩を踏み出しました:
クロージャは、その語彙的スコープの外で実行されても、その語彙的スコープにアクセスする関数です。
もっと簡単に言うと、クロージャは、後でどこで実行されるかに関わらず、定義された場所の変数を記憶する関数です。
クロージャを識別するための経験則ですが、関数内に外部変数(関数内で定義されていない)がある場合、外部変数が取り込まれているため、その関数はクロージャである可能性が高いです。
先ほどのコードでは、outerVar
outerFunc()
innerFunc()
の中にあるエイリアン変数です。
続いて、なぜクロージャーが便利なのかを示す例を見てみましょう。
5. クロージャの例
5.1 イベント ハンドラ
ボタンが何回クリックされたかを表示してみましょう:
let countClicked = 0;myButton.addEventListener('click', function handleClick() { countClicked++; myText.innerText = `You clicked ${countClicked} times`;});
デモを開き、ボタンをクリックします。 テキストが更新され、クリックされた回数が表示されます。
ボタンがクリックされると、DOMコードのどこかでhandleClick()
が実行されます。 その実行は、定義された場所から遠く離れたところで行われます。
しかし、クロージャであるため、handleClick()
countClicked
myText
もキャプチャされます。
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
iterator
countEven
という変数を捕らえているのでクロージャーとなります。
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)
a
a
b
a * b
が計算されます。
6. 結論
JavaScriptの変数のアクセス性を規定しているのはスコープです。 スコープには、関数やブロックのスコープがあります。
レキシカルスコープにより、関数スコープは外側のスコープから変数に静的にアクセスすることができます。
最後に、クロージャは、そのレキシカルスコープから変数を取り込む関数です。 簡単に言えば、クロージャはどこで実行されても、それが定義されている場所からの変数を記憶します。
クロージャは、イベントハンドラやコールバックの中で変数を捕捉します。 関数型プログラミングで使われています。
Frontendの面接では、クロージャの仕組みを聞かれることもあります。
すべてのJavaScript開発者はクロージャがどのように動作するかを知っておく必要があります。 対処してください⌐■_■.
チャレンジしてみませんか? JavaScriptクロージャーに関する7つの面接質問。 あなたはそれらに答えることができますか?