クロージャー(Closure)の概念は関数型プログラミングにおいて重要であり、JavaScriptの面接でよく質問される事もあります。
どこでも使用されていますが、クロージャーは把握するのがとても困難でもあります。
まず、クロージャーを学ぶまえに(スコープ)および(レキシカルスコープ)の基本を学ぶ必要があります。
そして、前提としてJSの変数および関数に精通しているかご確認下さい。
この記事では、JavaScriptクロージャーの仕組みおよびJavaScriptスコープについて解説します。
スコープ
JavaScriptのスコープ(Scope)は、変数や関数がどこからアクセス可能かを定義する仕組みです。
変数や関数がどこで宣言されたかによって、その変数や関数がどの範囲で有効かが決まります。
変数のアクセス可能性は、スコープによって管理されます。
そのスコープ内で定義された変数には自由にアクセスすることができます。
しかし、そのスコープの外では、その変数にアクセスすることができません。
JavaScriptでは、関数やコードブロックによってスコープが作成されます。
スコープは、グローバルスコープとローカルスコープの2つのスコープがあります。
・ グローバルスコープ
プログラムのどこからでもアクセスできる変数は、グローバルスコープに存在すると言われます。
グローバルスコープの変数は、let、const、および varの3つのキーワードのいずれかを使用して定義できます。
var scope = "Global"; console.log(scope) // Global function access() { console.log(scope) // グローバル変数へのアクセス } access() // Global
let scope = "Global"; const mainFunc = () => { console.log(scope) } mainFunc() // Global console.log(scope) // Global
See the Pen JavaScript global scope by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
ここで、scope
変数名はmainFunc関数内でも外部でもアクセスできます。
このscope
には、コード内のどこからでもアクセスできます。
したがって、scope
変数はグローバルスコープに存在します。
・ ローカルスコープ
一方、ローカルスコープは、関数内で宣言する変数はすべて、ローカルスコープを持つと言われます。
つまり、関数内でローカル変数にアクセスできます。
関数内で定義された変数に外部または別の関数からアクセスしようとすると、エラーがスローされます。
ローカルスコープの例を以下に示します。
function myFun() { let name = "deve.K"; console.log(name); } myFun(); console.log(name); //ReferenceError: name is not defined
See the Pen JavaScript Local scope by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
・ ブロックスコープ
2015年にES6(ECMAScript 6)が導入される前は、JavaScriptにはグローバルスコープとローカルスコープの2種類のスコープしかありませんでした。
letおよびconstキーワードの導入により、JavaScriptに新しいタイプのScopeが追加されました。
それがブロックスコープです。
これは、変数がブロックの中で宣言された場合に作られます。
そのブロックとは、中括弧{ }
で囲まれたコードブロックのことを指します。
特定のブロック({}で表される) 内で宣言された変数に、ブロックの外部からアクセスすることはできません。
function myFun() { if (true) { let x = 10; console.log(x); // 10 } console.log(x); // ReferenceError: x is not defined }
上記では、変数x
はif文のブロック内で宣言されており、そのブロック内でのみアクセスが可能です。
ローカルスコープと同じでは?と疑問に思われましたか?
あやふやにせず、明確に理解しましょう。
ローカルスコープとブロックスコープは、本質的には同じです。
ですが、ローカルスコープは、関数内で変数を定義し、その関数の内部でのみ、その変数にアクセスできるようにする方法として使用します。
ブロックスコープは、中括弧{ }
で囲まれたブロック内で変数を定義し、そのブロックの内部でのみその変数にアクセスできるようにする方法です。
ローカルスコープは、古くから存在している概念です。
ブロックスコープは、if文やforループなどのブロックスコープ内で変数を宣言することができますが、ローカルスコープは主に関数内で変数を宣言するために使用されます。
本質的には、ローカルスコープはブロックスコープでもあります。
ですが、ブロックスコープはより細かく範囲を制限するための機能として、ローカルスコープと同様の機能を持っています。
また、ブロックスコープは var
キーワードでは機能しません。
そのためにlet
またはconst
キーワードを使用できます。
・ 関数スコープ
関数スコープは、関数内で宣言された変数が関数の内部でのみアクセス可能であることを意味します。
関数スコープ内で宣言された変数は、関数の外部からはアクセスできません。
こちらも、関数スコープとローカルスコープは似ている概念ですが、わずかに異なる点があります。
関数スコープが変数の利用可能性にどのような影響を与えるか見てみましょう。
以下の変数は、関数num()で作られたスコープに属しています。
function num() { // function scope let count = 0; console.log(count); // 0 } num(); console.log(count); // ReferenceError: count is not defined
count
変数は、num()の範囲内で自由にアクセスすることができます。
しかし、num()のスコープ外では、count
はアクセスできません。
とにかく外からcount
にアクセスしようとすると、JavaScriptはReferenceError: count is not defined
を投げます。
関数やコードブロックの内部で変数を定義した場合、この変数はその関数やコードブロックの内部でのみ使用することができます。
ローカルスコープと関数スコープの違いは、ブロックと関数のスコープの違いにあります。
先述したように、ローカルスコープはブロックスコープであり、関数スコープは関数内のスコープに限定されます。
つまり、変数がどの範囲で有効かを制御する方法に違いがあるという事です。
ブロックスコープは、中括弧{ }
で囲まれたブロック内でのみ有効であり、関数スコープは、関数内でのみ有効であるという違いです。
また、スコープが変数を分離する、という性質がございます。
異なるスコープが、同じ名前の変数を持つことができるので、これは素晴らしいことです。
異なるスコープで共通の変数名(count、index、current、valueなど)を衝突することなく再利用することが可能ということです。
下記でのfoo()
とnum()
の関数スコープは、それぞれ独自の、同じ名前のcount
変数を持ちます。
function foo() { // "foo" function scope let count = 0; console.log(count); // logs 0 } function num() { // "num" function scope let count = 1; console.log(count); // logs 1 } foo(); num();
See the Pen JavaScript 関数スコープの変数の分離 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
foo()
とnum()
の関数スコープからのcount
変数が衝突しません。
つまり、関数スコープ内で同じ変数名が宣言された場合、その変数は同じ名前でも別の変数として扱われます。
このように、関数スコープは変数を分離します。
関数スコープが変数の分離にどのように影響を与えるかを理解することは、変数名の重複や意図しない変更を防止するために非常に重要となっています。
このような原則を守ることで、コードの保守性を高めることができます。
スコープチェーン
関数が別の関数内にネストできるように、スコープは互いにネストできます。
つまり、あるスコープを別のスコープにネストすることです。
const name = 'Taro'; function outerFun() { const name = 'Hanako'; function logName() { console.log(name); } logName(); } outerFun(); // 'Hanako'の変数を上書き // local scopeのみ console.log(name); // 'Taro' global変数
上記のname
変数名はグローバル変数です。
outerFun()
のスコープ内で再宣言されると、同じ名前の変数で新しい値を持つ、新しいローカル変数が作成されます。
outerFun()
のスコープ内では、ローカル変数名が使用されます。
変数が初期化されるたびに、自分のスコープでその変数を探します。
ローカルスコープで変数が見つからなければ、親スコープで検索します。
親スコープで見つからなければ、祖父母スコープを探し、グローバルスコープに到達するまで続けます。
親スコープ(外部スコープ)の変数は、子スコープ(内側スコープ)の内部でアクセスできますが、その逆はできません。
・スコープはネスト可能
・外側のスコープの変数は、内側のスコープ内でアクセスできます。
では、レキシカルスコープについて説明致しますが注意点がございます。
関数スコープとレキシカルスコープは非常に似ており混同してしまう可能性がありますが、微妙に異なる概念です。
そこを念頭に置いて学習に進んで下さい。
レキシカルスコープ
JavaScriptでは、レキシカル・スコープ(Lexical Scope)または静的・スコープと呼ばれるスコープ機能を実装しています。
JavaScriptにおけるレキシカルスコープとは、変数の有効範囲が定義される方法の一つで、コードの記述時点でスコープが決まることを意味します。
つまり、レキシカルスコープとは、変数が存在する領域が、変数が定義または作成された場所によって決定されることを意味しているという事です。
変数が、そこに呼び出された場所ではなく、作成されたスコープでのみ使用可能です。
簡単に言えば、レキシカルスコープとは、内側のスコープ内で、外側のスコープの変数を使用できることです。
let myOuterVar = "Hello"; function myFun() { let myInnerVar = "World"; console.log(myOuterVar + " " + myInnerVar); document.write(myOuterVar + " " + myInnerVar) } myFun(); // 出力: "Hello World" console.log(myOuterVar + " " + myInnerVar); // エラー: myInnerVar is not defined
See the Pen JavaScript lexical scope by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記の例では、letキーワードを使用してmyInnerVar
を宣言し、myFun
関数内で使用しています。
これにより、myInnerVar
はmyFun
内のブロックスコープ内でのみ有効になります。
関数の外側でmyInnerVar
変数を使用しようとすると、同様に"myInnerVar is not defined"
というエラーが発生します。
これで、レキシカルスコープは変数のスコープを決定する仕組みだという事が理解できたかと思います。
つまり、レキシカルスコープとは関数がどこに宣言されたかに基づいて、その関数が参照できる変数の範囲が決まります。
関数スコープでは、関数内で定義された変数がその関数内でのみアクセス可能であることを指します。
関数の外部で、同じ名前の変数を定義しても、関数内での変数とは別の変数として扱われます。
そこの違いとなります。
letキーワードを使用すると、変数のスコープをより細かく制御できます。
ブロックスコープの変数を使用することで、コードの読みやすさを向上させることができます。
JSのクロージャーとは?
JavaScriptにおけるクロージャー(Closure)と閉鎖は、実際には同じ概念を指しています。
クロージャーは、外部スコープにある変数にアクセスできるようにすることで、その変数を保護する機能を提供することができます。
その状態を「閉じられた状態」と表現します。
そのため(閉鎖)または(クロージャー)と呼ばれます。
したがって、クロージャーと閉鎖は同じものを指し、どちらもJavaScriptにおいて、外部スコープにある変数を内部スコープで保護するために使用されます。
クロージャーは、レキシカルスコープの外で実行された場合でも、そのレキシカルスコープにアクセスする関数となります。
つまり、外側の関数が実行を終了しても、その内側の関数が変数や関数を参照し続けるため、外側の関数内で定義された変数や関数が保持された状態を維持します。
この機能により、変数や関数を外部から保護したり、プライベート変数を定義したりすることが可能となります。
重要な事は、実行される場所に関係なく外部関数の実行後でも可能となります。
function createCounter() { let counter = 0; function increment() { counter = counter + 1; console.log(counter); } return increment; } const updateClickCounter = createCounter(); updateClickCounter(); // 1
See the Pen JavaScript クロージャー by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記のように、クロージャーとは、関数が常にその周囲にアクセスできるという事です。
外側の関数を呼び出すと、counter
変数が(0)に設定されます。
したがって、外側の関数が呼び出された後は、counter
変数が使用できないと予想される場合があります。
しかし、変数は外側の関数のスコープ内にあります。
スコープはネストできるため、内部関数は引き続き counter
変数にアクセスできます。
コードの一部が内部関数を呼び出すことができる限り、変数のcounter
を記憶することができます。
クロージャにより、関数はそのスコープ内の変数を記憶でき、スコープを使用すると関数とその外部にアクセスできるデータを組み合わせることができます。
もう少し、簡単に説明致しましょう。
前述した通り、クロージャーとは、関数が外側のスコープの変数にアクセスできることです。
関数の中に関数を作成してみましょう。
outer()
関数がinner()
関数を返しています。
outer()
関数を呼び出し、その結果を定数に代入したのちに、定数に格納されている関数を呼び出しましょう。
function outer() { //外側 const a = "Hello "; function inner() { //内側 const b = "World!!"; console.log(a + b); } return inner; } const Greetings = outer(); Greetings(); // Hello World!!
See the Pen JavaScript クロージャー ② by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記で確認してみると、定数の(a)がinner()
関数の外側で定義されていることがわかります。
outer()
関数呼び出しが終了すると、定数(a )の痕跡はなくなります。
これは、Greetings()
を呼び出すと、(b)のみが定義されているときに(a + b)をログに記録しようとしているため、エラーが表示されると想定されます。
定数(a )がinner()
関数の外で指定されています。
しかし、ご覧のとおり、Greetings()
は明らかに(a)と(b)の両方にアクセス可能となっています。
他の例でも見てみましょう。
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } innerFunc(); } outerFunc();
innerFunc()
スコープ内では、レキシカルスコープからouterVar
変数にアクセスされます。
innerFunc()
の呼び出しは、そのレキシカルスコープ(outerFunc()のスコープ)内で行われることに注意してください。
innerFunc()
を、そのレキシカルの範囲外の関数exec()
で呼び出すという、コード変更をしてみましょう。
その場合でもinnerFunc()
はouterVar
にアクセスできるのでしょうか?
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } return innerFunc; } function exec() { const myInnerFunc = outerFunc(); myInnerFunc(); } exec();
上記のように、innerFunc()
は、そのレキシカル・スコープの外で、exec()
関数のスコープ内で実行されます。
そして、重要な事は、innerFunc()
は、レキシカルスコープ外で実行されても、そのレキシカルスコープからouterVar
にアクセスすることができます。
言い換えれば 、innerFunc()
は、outerVar
変数をレキシカル・スコープから閉じるため、これはクロージャーです。
クロージャーは、内部関数が周囲のコードブロックがなくなった後でも、その周囲のコードブロック内の変数にアクセスできることを意味している事が理解できたかと思います。
プライベート変数
クロージャーの優れている点が、モジュールのカプセル化を実装する機能です。
スタックのデータ構造を実装するタスクがあるとします。
本来であれば、アイテムをスタックにpush(プッシュ)またはpop(ポップ)することしかできません。
通常のJavaScript配列は、array.push()
メソッドとarray.pop()
メソッドの両方を提供しています。
新しい要素をスタックの一番上にpushしていき、popしてスタックに挿入された要素を削除できます。
これは一般的なデータ構造の1つであるStuckです。
以下の例をご確認下さい。
function Stack() { const items = [] return items; } const stack = Stack(); stack.push(3) stack.push(2) stack.push(1) console.log(stack.pop()); // 1 stack.length = 0; // stackを削除 console.log(stack.pop()); // undefined
上記は問題がございます。
それは、配列オブジェクト全体がエクスポートされるため、stack.length = 0
を使用してスタックを消去したり、スタック上で通常許可されない操作を簡単に行うことができてしまいます。
つまり、popではなく(.length = 0)でも削除が許可されます。
これを避けるために、上記のスタックの実装をカプセル化し、 pushかpopの操作しかできないようにする事が可能です。
それが、クロージャーが役に立つ所となります。
function Stack() { const items = []; return { push(item) { items.push(item); }, pop() { return items.pop(); } } } const stack = Stack(); // スタックのインスタンスを作成 stack.push(3); // スタックに要素 3 を追加 stack.push(2); // スタックに要素 2 を追加 stack.push(1); // スタックに要素 1 を追加 console.log(stack.pop()); // スタックから要素を取り出し、結果を表示 (出力: 1) stack.length = 0; // スタックの長さを 0 に設定 (スタックの内容は影響を受けません) console.log(stack.pop()); // スタックから要素を取り出し、結果を表示 (出力: 2)
上記のように、push()とpop()メソッドは、items配列にアクセスするクロージャとして機能しています。
items変数は、Stack()関数のローカルスコープ内で定義されているため、外部から直接アクセスすることはできません。
これにより、items変数はprivate変数となります。
push()メソッドは、外部から渡された要素をitems配列に追加します。pop()メソッドは、items配列の最後の要素を取り出して返します。
このように、items配列に対する変更やアクセスは、Stack()関数内で定義されたpush()とpop()メソッドを介して行われます。
console.log(stack.pop()) によって、スタックから要素が取り出され、結果が表示されます。この場合、スタックの最後の要素である1が取り出されるため、1が出力されます。
(stack.length = 0)によって、スタックの長さが0に設定されますが、スタックの内容自体は影響を受けません。
したがって、次のconsole.log(stack.pop())では、スタックは空の状態ですので、何も取り出すことができず、undefinedが出力されます。
これにより、スタックの内部のデータを保護し、外部からの不正な操作を防ぐことができます。
このアプローチはプライベート変数による、カプセル化を実現するためにクロージャを使用できる素晴らしい例となっています。
プライベートメソッド
JavaScriptでは、JavaやC++などの他の言語のように、すぐに使用できるプライベートメソッドを作成する方法はありません。
それらが属するクラスであり、外部から直接アクセスおよび変更することはできません。
しかし、クロージャーを使用してプライベート変数とセッターおよびゲッター関数を使用して基本的なファクトリ関数を作成できます。
下記は、クラスやコンストラクターとは異なる点に注意して下さい。
オブジェクトを返す関数にすぎません。
function Private(name, job) { let _name = name; // "Private"変数 // 変数の公開 return { // Getter メソッド getName() { return _name; }, // Setter メソッド setName(newName) { _name = newName; } } } const myName = Private("deve.K") console.log(myName.getName()) // deve.K console.log(myName._name) // undefined maName.setName("Taro") console.log(myName.getName()) // Taro
プライベート変数の前に_
を付けるのは良い習慣です、ゲッター関数のgetName
、セッター関数のsetName
を持つシンプルな Factory Function Privateを作成しています。
name
に直接アクセスしようとすると、関数スコープ内にあるため、これは外部からアクセスできずにundefined
が返されます。
そこで、クロージャーを使えば、プライベート変数を実行させることができます。
JavaScriptのクロージャーを使用することで、オブジェクト指向プログラミングの概念である、カプセル化と相性が良く、コードの保守性や再利用性を向上させることができます。
それでは、クロージャーを使用して、ボタンをクリックした回数をカウントする例を見ていきましょう。
クロージャーを使用して、イベントハンドラー内で変数を保持し、その値を更新することが可能です。
<button type="button" id="myButton">Click me</button><br /> <span id="myText">You clicked 0 times</span>
function addClickHandler() { let count = 0; const button = document.getElementById('myButton'); const myText = document.getElementById("myText"); button.addEventListener('click', function() { count++; console.log(`Button clicked ${count} times.`); myText.innerText = `You clicked ${count} times.`; }); } addClickHandler();
See the Pen JavaScript closure Event handler by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
ログとブラウザ上、両方で確認できるようになっております。
addClickHandler()関数内でcount変数を定義し、その後、イベントハンドラー内で使用しています。
count
変数は、クロージャー内で定義されているため、イベントハンドラーが実行されるたびに、その値を保持しています。
また、letキーワードを使用しているため、count
変数はブロックスコープを持ちます。
つまり、addClickHandler()関数の外部からはアクセスできません。
このように、クロージャーを使用することで、イベントハンドラー内で変数を保持し、その値を更新することができます。
下記の場合でも、ボタンがクリックされると、DOMコードの中のどこかでhandleClick()関数が実行されます。
const button = document.getElementById('myButton'); const myText = document.getElementById("myText"); let count = 0; button.addEventListener('click', function handleClick() { count++; console.log(`Button clicked ${count} times.`); myText.innerText = `You clicked ${count} times.`; });
See the Pen JavaScript closure Event handler ② by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
定義した場所から遠く離れた場所で実行されています。
しかし、クロージャーであるhandleClick()は、レキシカルスコープからcount
を記憶し、クリックが発生するとそれを更新します。
さらに言えば、myText
も記憶されます。
レキシカルスコープとクロージャーの違い
レキシカルスコープとクロージャーの明確な違いを整理しときましょう。
JavaScriptのクロージャーとレキシカルスコープは密接に関連しておりますが、異なる概念です。
レキシカルスコープは、コードの静的な構造に基づいて、どの変数がどのスコープに属するかを決定する仕組みです。
つまり、関数がどこに宣言されたかに基づいて、その関数が参照できる変数の範囲が決まります。
この仕組みによって、変数の名前が衝突することを避けたり、関数の実行中に正しく変数を参照することができます。
一方、クロージャーは、関数が実行される環境(スコープ)を記憶して、その環境にある変数を参照できるようにする仕組みです。
クロージャーを使うことで、関数が外部のスコープにある変数を参照したり、変更したりできます。
これにより、関数が実行されるたびに新しい環境を作成することができます。
つまり、レキシカルスコープは変数のスコープを決定する仕組みであり、クロージャーは関数が外部のスコープにある変数を参照できるようにする仕組みです。
ですが、クロージャーはレキシカルスコープに依存しているため、両者はかなり密接に関連していることに注意してください。
クロージャーの理解が分かった所で、JavaScriptでのクロージャーの利点は何でしょうか?
JSでのクロージャの利点と欠点
クロージャーには、JavaScriptプログラミングにおいていくつかの利点があります。
その主な利点の1つは、プライベート変数とデータのカプセル化を作成できることです。
これは、クロージャー内の変数と関数がクロージャーの外部からアクセスできないことを意味し、プログラムの他の部分によって変更またはアクセスされるのを防ぎます。
クロージャーのもう1つの利点は、コードの重複を減らし、再利用可能なコードを作成するために使用できることです。
クロージャー内に機能をカプセル化することで、複数の場所でコードを繰り返すことを避け、よりモジュール化された保守しやすいコードを作成できます。
そして、メモリ効率の向上にもなります。
クロージャーは、関数の外部から変数にアクセスするために、変数をグローバルスコープに定義する必要がありません。
そのため、関数内で必要な変数だけを定義することができ、メモリ効率が向上することがあります。
ただし、クロージャーにはいくつかの欠点もあります。
主な欠点の1つは、デバッグの複雑さです。
クロージャーはスコープ外の変数にアクセスできるため、コード内のバグやエラーを追跡するのが難しい場合があります。
クロージャーをデバッグするためのヒントは、console.log
の使用と、デバッガーを使用したコードのステップ実行が含まれます。
また、バグやエラーのリスクを最小限に抑えるために、クロージャーをできるだけシンプルで焦点を絞ったものにすることも重要です。
クロージャーを誤用すると、予期しない問題が発生することがあります。
そのため、クロージャーを正しく理解し、適切に使用することが重要となります。
最後に
JavaScriptプログラミングでクロージャーを使用する場合は、いくつかのベストプラクティスに従って、コードを保守しやすく理解しやすいものにすることが重要です。
ベストプラクティスの1つは、クロージャーをできるだけ小さくし、焦点を絞ることです。
これは、クロージャー内に機能を詰め込みすぎないようにし、その代わりとして、より小さく、より焦点を絞ったクロージャーに分割する必要があります。
もう1つのベストプラクティスは、クロージャーの外部で変数を変更しないようにすることです。
これによって、コードで予期しない動作やバグが発生する可能性があります。
ですので、変数をパラメーターとしてクロージャーに渡し、必要に応じてクロージャーから値を返します。
クロージャーを使用するためのベストプラクティスには、プライベート変数、カプセル化、およびコードの再利用性のためにクロージャーを使用することが含まれます。
不必要なクロージャーを作成しないようにし、メモリ使用量を適切に管理することも重要です。
クロージャーは、高度なJavaScriptプログラミングを行う際に理解する必要のある概念です。
これら、ベストプラクティスに従い、クロージャーを慎重に計画することで、JavaScriptの強力な機能を最大限に活用できるようになるはずです。
本日は以上となります。
最後まで記事を読んで頂きありがとうございます。
この記事が気に入ったらブックマークし、他の方にも共有してください。