初めに
何かわかるけど実際分かっていない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