初めに
何かわかるけど実際分かっていないClosure、今回も自分なりに自分の理解をまとめていきたいと思います。
主な参考資料はこちらです↓
You Don't Know JS: Scope & Closures - 1st Edition
そしてTL;DRの方へ
Closureとは
まずMDNの説明文から...
クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript では、関数が作成されるたびにクロージャが作成されます。
クロージャ - JavaScript | MDN
少し調べたら、思ったよりJavaScriptエンジンにある深層部の知識がたくさん関連していることが分かって、ScopeとLexing scopeの概念も一緒にまとめたいと思います。
Scope and Lexing Scope
主な参考資料はこちらです↓
Chapter 1: What is Scope?
Chapter 2: Lexical Scope
Chapter 5: Scope Closure
Scope
自分の理解で要約してみると、Scopeというのは変数の貯蔵庫であり生存領域として、コードを実行する前に、Compiler(JavaScriptならJavaScriptエンジン)がcompilationを行う場所です。
そしてcompilationは三つのステップで構築されている。
- Tokenizing/Lexing:ソースコードの文字列を解析してトークンを得る。
var a = 2;⇒var,a,=,2,; - Parsing:トークンで抽象した木構造のツリー(Abstract Syntax Tree, AST)へ構築していく。
var:VariableDeclaration(変数宣言)
a:Identifier(識別子)
=:AssignmentExpression(代入式)
2:NumericLiteral(数字リテラル)
var
/ \
a =
|
2
- Code-Generation:この段階では上のASTを執行可能の機械言語に変換する。
結果としては、varで変数宣言し、aという変数に、数字2を入れて保存する。
さらにプログラムを実行する一連の処理はエンジン、コンパイラ、スコープが互いに支えあって各自の役割を果たしている。
- Engine:初めの
compilationから最後のexecutionまで、プログラムを実行する任務を任される。 - Compiler:
compilationの三つのステップを経てコードを処理する。 - Scope:宣言された識別子(変数の
aとか)のリストをしっかりと確立し管理する。
さきほどvar a = 2;の例では、コード実行すると、
まずCompilerがScopeに、aという変数がすでにある特定のスコープに生存しているかと問いかけ、もし答えがYesならaの変数宣言を無視して次に進む。もし答えがNoならaという新しい変数を設ける。
そして次a = 2代入式の部分では、EngineはScopeに、このスコープでaという変数がアクセスできるかと聞き、YesならEngineは変数を利用する。Noなら、Engineはほかのネストしたスコープ(自分より上のスコープ)をチェックしていく。
変数見つかったら2を与える。どこでも見つからなかったら、Engineはエラーと示してくる。
function outerFn() {
var a = 1
function innerFn() {
var b = 2
console.log('a:', a, 'b:', b, 'a+b:', a + b)
}
innerFn()
}
outerFn() // a: 1 b: 2 a+b: 3
Lexing Scope
ほかのプログラミング言語ではScopeを静的スコープLexing Scopeと動的スコープDynamic Scopeに分けられ、しかしJavaScriptはLexing Scopeしか持っていません。
Lexing Scopeはvar a = 2;のような文字列を解析しトークンを得るcompilationの第一ステップです。
そしてどこのスコープでcompilationを実行するかはコードを書くときすでに決定されている。
function outerFn(a) {
const b = a * 2
function innerFn(c) {
console.log('innerFn a:', a) // innerFn a: 5
console.log('innerFn b:', b) // innerFn b: 10
console.log('innerFn c:', c) // innerFn c: 30
}
innerFn(b * 3)
console.log('outerFn a:', a) // outerFn a: 5
console.log('outerFn b:', b) // outerFn b: 10
console.log('outerFn c:', c) // ReferenceError: c is not defined
}
outerFn(5)
// innerFn a: 5
// innerFn b: 10
// innerFn c: 30
// outerFn a: 5
// outerFn b: 10
// ReferenceError: c is not defined
global scopeに識別子がただ一つouterFnで、
outerFn scopeにa, b, innerFn識別子、
innerFn scopeにc識別子があります。
ネストしたスコープは自分より外/上のスコープ(outward/upward)へ変数や値などアクセスできるが、自分より下のスコープへアクセスできない特性を持っているので、outerFnでcを出力することが不可能です。
そしてスコープ範囲内で識別子を探すのはshadowingと呼ばれて、同じ変数が別々のスコープに存在しても、識別子にたどり着いたらすぐストップする。
function outerFn(a) {
const b = a * 2
console.log('outerFn a:', a) // innerFn a: 3
console.log('outerFn b:', b) // innerFn b: 6
function innerFn(c) {
const b = a * 10
console.log('innerFn a:', a) // innerFn a: 3
console.log('innerFn b:', b) // innerFn b: 30
}
innerFn(b * 3)
}
outerFn(3)
結局、Closureって何?
example1のように、innerFnで変数aの値を出力するから、outerFnの変数aへアクセスし、RHSを通して値をゲットするのがClosure?
確かに私もそう理解してきていますが、参考文章によるとこれは不完全な解釈のようだった。
私たちがみた内部関数が外部変数の値への要請は、正確に言えばlexical scopeのlook-upルールを通してClosureになった一部の結果にすぎません。
function outerFn() {
const a = 1
return function innerFn() {
console.log('innerFn a:', a)
}
}
const test = outerFn() // outerFn() returns innerFn
test()
変数testにouterFn()を入れたのは、outerFn関数ではなくouterFn()関数の実行した結果、つまりinnerFn関数です。
そしてinnerFn関数は変数aを出力ということで、lexical scopeのlook-upルールを通して、outerFn関数の変数aへたどり着くんです。
Engineはメモリー回収という機能を持ち、不要なものをクリーニングするのですが、innerFn関数が値として変数testへ入れる状態では、innerFn自身も、変数aが生存しているouterFnからメモリー回収が不可能なので、Closure状態になっている。
TL;DR
簡潔に言うと、returnの値は関数でそして外部の変数を使っているならClosure状態になります。
おまけ
いろいろ試行錯誤の中書いてみたコード例です。
例を参考した文章はこちらです↓
Closures and currying
const closureFunction = someVariable => {
let scopedVariable = someVariable
const closure = () => {
scopedVariable++
console.log(scopedVariable)
}
return closure
}
let testOne = closureFunction(1)
testOne() // 2
testOne() // 3
let testTwo = closureFunction(10)
testTwo() // 11
testTwo() // 12
testOne() // 4
// the same
let scopedVariable = 0
const closureTest = function closure() {
scopedVariable++
console.log(scopedVariable)
}
closureTest() // 2
closureTest() // 3
scopedVariable = 10
closureTest() // 11
closureTest() // 12