Closureの説明は非常に分かりにくいものが多いのですが、段階を踏むと必ず理解できます。
Closureを理解する前提として、下記の知識が少しでも曖昧だと厳しいと思いますので、まず、これらの内容を整理して、しっかり理解しましょう。
・JavaScriptにおけるスコープとは?
・何がスコープを作るのか?
・ファンクションが入れ子になった場合のスコープはどうなっているか?
・varとlet/constのスコープにおける違い
この投稿では、上記の内容がだいたいわかっているものとして、説明させていただきます!
Closure
下記のコードを見てみましょう。
function outerFunc() {
var monster = 'Mozilla';
function innerFunc() {
//この下にある変数monsterは、2行目のvar monster = 'Mozilla'
//にある変数monsterに対してスコープを持っている
console.log(monster);
}
innerFunc();
}
outerFunc(); //コンソール出力はMozilla
- 最後の行にあるouterFunc()により、ファンクションouterFuncが起動されると、outerFuncのファンクションボディ(波括弧{ }に挟まれたエリア)におけるコードが順番に実行されます。
- outerFuncのファンクションボディ内にあるファンクションinnerFuncもinnerFunc()により起動されます。
- 最終的には、console.log(monster)のmonsterが2行目のmonsterの値(var monster = 'Mozilla')を参照しているため、Mozillaがコンソール出力されます。
このように、outerFuncを起動した際に作られる、innerFuncのファンクションボディから見たouterFuncのファンクションボディに対するスコープ・チェイン(スコープの連鎖)をClosureといいます。
Lexical Scoping
下記のコードは、上記のClosureの説明の際に利用したouterFuncをベースにして変更したものですが、このような形でファンクションを入れ子にして、outerFuncのリターンを利用して内側にあるinnerFuncをまるごと外に放り出し、それを新しい変数に代入し、最後にその変数を利用してリターンされたinnerFuncを実行すると、少し不思議なことが起こります。
function outerFunc() {
var monster = 'Mozilla';
function innerFunc(newMonster) {
//この下の行にある変数monsterは、2行目のアウタースコープにあるmonsterに対してスコープを持っている
newMonster ? monster = monster + newMonster: null;
console.log(monster)
}
return innerFunc;
}
//上記のファンクションinnerFuncをまるごとmyFuncへ代入します
var myFunc = outerFunc();
myFunc(); //コンソール出力はMozilla
myFunc("Gozilla"); //コンソール出力はMozillaGozilla
myFunc("Vanilla"); //コンソール出力はMozillaGozillaVanilla
- 上記のコードですが、innerFuncの中の変数monsterは、2行目で宣言している変数monsterに対してスコープを持っています。
- outerFuncを起動すると、戻り値としてファンクションinnerFuncを返してきますので、そのinnerFuncをまるごと新たな変数(上記では、myFunc)に代入することができます。
- innerFuncを代入した変数(ここではmyFunc)を利用して、innerFuncをouterFuncの外側で実行することができますので、それを実行します。
- その際、outerFuncの外側で実行されたinnerFuncの中にある変数は、引き続きouterFuncの中で宣言されている変数monsterへのスコープを持っています。(Lexical Scoping)
よくわからない言葉(Lexical Scoping)・・・なんそれ?
言葉の意味をおさらいしますと、
Scope
銃の照準器のことをスコープといいますが、一言で言うと、「そこから見える範囲」のことです。また、JavaScriptとしては、「見える」とは、「ここからその変数の値を参照できる」という意味になります。
Closure
ファンクションが入れ子になっている場合、内側のファンクションから見ると、自分自身のファンクション・ボディのスコープ、外側ファンクション・ボディへのスコープ(outer scope)、グローバルスコープへのスコープといったスコープの3階建てのような形になりますが、そのように、あるファンクションが、グローバルスコープから見えないアウタースコープを持つ場合、その状態をclosureと呼びます。
Lexical Scoping
lexicalという形容詞の意味を辞書で引くと、
- of or relating to words or the vocabulary of a language as distinguished from its grammar and construction
- of or relating to a lexicon or to lexicography
ということで、何のことだかよくわかりません。
JavaScripにおけるlexical scopingの意味ですが、ファンクションが入れ子になっている場合、たとえ内側ファンクションを外側ファンクションの外で実行したとしても、当初の位置関係(リターンされる際のオリジナルの位置関係)に基づくスコープを持っている、そんな感じで考えておきましょう!
lexical scopingとは、static scopingとも呼ばれており、上記のサンプルコードのようにファンクションが入れ子になっているケースにおいて、
- 外側ファンクションを起動
- 外側ファンクションのリターンで内側ファンクションをまるごとアウトプット
- アウトプットされた内側ファンクションを外側ファンクションの外にある新たな変数に代入
を実施した場合、その新たな変数に代入された内側ファンクションは、上記の1番で外側ファンクションが実行された際の位置関係におけるスコープを保持しています。
上記のサンプルコードにおいて、outerFuncを2回実行して、innerFuncを別々の変数にアサインするとどうなるか?
function outerFunc() {
var monster = "Mozilla";
function innerFunc(newMonster) {
//この下の行にある変数monsterは、2行目のアウタースコープにあるmonsterに対してスコープを持っている
newMonster ? monster = monster + newMonster: null;
console.log(monster)
}
return innerFunc;
}
//outerFuncを実行し、innerFuncをmyFuncへ代入します。
var myFunc = outerFunc();
myFunc(); //コンソール出力はMozilla
myFunc("Gozilla"); //コンソール出力はMozillaGozilla
myFunc("Vanilla"); //コンソール出力はMozillaGozillaVanilla
//今度は、もう一度outerFuncを実行し、innerFuncを異なる名前の変数myFunc2へ代入します。
var myFunc2 = outerFunc();
myFunc2(); //コンソール出力はMozilla
myFunc2("Motor"); //コンソール出力はMozillaMotor
myFunc2("Mosaic"); //コンソール出力はMozillaMotorMosaic
//念のためもう一度
myFunc("Ballet"); //コンソール出力はMozillaGozillaVanillaBallet
myFunc2("Marina"); //コンソール出力はMozillaMotorMosaicMarina
上記の結果の通りですが、結論としては、変数myFuncに代入されたinnerFuncと変数myFunc2に代入されたinnerFuncはそれぞれ全く独立したスコープを持っていて、アウタースコープにおいて参照しているvar monster = "Mozilla"の変数monsterについても、独立した値を保持しています。
つまり、outerFuncを実行してinnerFuncを新たな変数に代入する度に、新たなスコープと新たなローカル変数(monster)を生成しているということになります!
Closureを使って何かいいことありますか?
よく説明で出てくるパターンとして、ボタンをクリックすると1ずつ加算されるカウンターアプリの例があります。
カウンターアプリの必要な要素としては、下記のものになります。
- カウント数を保持するための変数を使い、初期値は0とする。
- カウント数を保持する変数は、間違って変更されないようにグローバル変数は使わず、ローカル変数とする。
- ボタンをクリックするたびに1ずつ加算するファンクションを作り、更新された値を画面上に表示する。
2番の制約がなければ、比較的簡単に作成できますが、この3つの条件をすべて満たすものを作成しようとしますと、工夫が必要になり、Closureの仕組みを使って構成することがその解決策になります。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>JavaScript Closure</h2>
<p>Counting with a local variable.</p>
<button type="button" onclick="myFunction()">COUNTER</button>
<p id="demo">0</p>
<script>
function adder () {
let counter = 0;
function countFunc (){
counter++;
return counter;
}
return countFunc;
}
//新たな変数additionへファンクションcountFuncを代入
//その代入されたcountFuncは、let counter = 0に対してスコープがあり、
//事前にアクションが記述されている保護された変数として利用できる
const addition = adder();
function myFunction(){
document.getElementById("demo").innerHTML = addition();
}
</script>
</body>
</html>
このように、closureのへんてこな特徴を利用すると、「グローバル・スコープから保護されているアウタースコープにある変数が利用できるファンクション」として活用することができますので、一つの方法として覚えておきましょう!
オブジェクト・メソッドを利用したClosure
下記のサンプルプログラムは、オブジェクト・メソッドを利用したClosureの例となります。
ファンクションclosureの中のgetFullDate, setFullDateなどは、オブジェクト・メソッドになっていますが、要するにファンクションなので、ファンクションが入れ子になっている状態です。
- ファンクションclosureを実行します
- そうすると、ファンクションclosureの中にあるオブジェクトobjがリターンされ、オブジェクトobjが新たな変数capsuleに代入されます。
- objが代入された変数capsuleを利用して、capsule.setFullDate("Sep 24th 2021")などとすると、ファンクションclosureの中のobjのメソッドであるsetFullDateがもっていたスコープを利用できますので、変数fullDateの値を書き換えることができます。
といったような方法で、一見不思議な「ファンクション内部で宣言した変数の値をそのファンクションの外から書き換える」といったことが可能になります。
console.time('Wow');
//下記のファンクションclosureの中にあるファンクション(getFullDate, setFullDate, setExplanationなど)
//は、その上の方で宣言されているfullDateやexplanationに対してスコープを持っています。
function closure(){
let fullDate = "not yet assigned";
let explanation = "no explanation";
let counter = 0;
let obj = {
getFullDate:() => fullDate,
getExplanation:() => explanation,
getCounter:() => counter,
setFullDate:(data) => fullDate = data,
setExplanation:(data) => explanation = data,
setCounter:() => counter++
}
return obj;
}
//closureを実行して、オブジェクトobjを変数capsuleに代入します
const capsule = closure();
//ちなみにconst capsule = Object.assign({}, closure())でも同じ結果が得られる
console.log('capsule:',capsule);
//一旦、ファンクションclosureの中にあるfullDate, explanation, counterの値を見てみましょう
console.log('ファンクションclosureの中の変数の初期値を確認します')
console.log('capsule.getFullDate():',capsule.getFullDate())
console.log('capsule.getExplanation():',capsule.getExplanation())
console.log('capsule.getCounter():',capsule.getCounter())
//ファンクションclosureの中の変数fullDateへ値を入れたいので、ひとまず、日付テキストを生成します
//次に、capsuleに代入されたsetFullDateの機能を使って、ファンクションclosureの中にある変数fullDateへ
//生成した日付テキストを代入します
let d = new Date();
let fullDateText = d.toString();
capsule.setFullDate(fullDateText);
//ファンクションclosureの中の変数explanaitonへ値を入れたいので、
//ひとまず、意味のないアルファベットの文字列を作成して、それを変数lettersに代入します。
const alphabet = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
const alphabetLength = alphabet.length;
let letters = "";
let length = Math.floor(Math.random() * 100);
length === 0 ? length = 1 : null;
console.log('length',length);
for (let i = 0; i < length; i++) {
const randomNumber = Math.floor(Math.random() * 100);
let remaining = randomNumber % alphabetLength;
letters = letters + alphabet[remaining];
}
console.log('letters:',letters);
//capsule.setExplanation()を使って、変数lettersの値を
//ファンクションclosureの中の変数explanationへ代入します
capsule.setExplanation(letters);
//ファンクションclosureの中の変数counterへ値を入れたいので、
//まず、何回カウンターをたたくのか、Math.random()を使って、2桁の整数を作ります
let times = Math.floor(Math.random() * 100);
console.log('times',times);
//forLoopを使って、その2桁の整数回capsule.setCounter()をinvokeします。
for (let i = 0; i < times; i++) {
capsule.setCounter();
}
//最後に、もう一度、ファンクションclosureの中にあるfullDate, explanation, counterの値を見てみましょう!
console.log('もう一度、ファンクションclosureの中の変数の値を確認します')
console.log('capsule.getFullDate():',capsule.getFullDate())
console.log('capsule.getExplanation():',capsule.getExplanation())
console.log('capsule.getCounter():',capsule.getCounter())
console.timeEnd('Wow')
補足情報
上記のサンプルコードは、Node.js v14.15.5でテストしたものです。また、私が利用している環境(Ubuntu + Node.js)では、処理時間は9ms ~ 10msとなりました。
参考情報