對(duì)于x年經(jīng)驗(yàn)的前端仔來說三圆,項(xiàng)目也做了好些個(gè)了狞换,各個(gè)場(chǎng)景也接觸過一些避咆。但是假設(shè)真的要跟面試官敞開來撕原理,還是有點(diǎn)慌的修噪〔榭猓看到很多大神都在手撕各種框架原理還是有點(diǎn)羨慕他們的技術(shù)實(shí)力,羨慕不如行動(dòng)黄琼,先踏踏實(shí)實(shí)啃基礎(chǔ)樊销。嗯...今天來聊聊閉包!
講閉包的文章可能大家都看了幾十篇了吧脏款,而且也能發(fā)現(xiàn)围苫,一些文章(我沒說全部)行文都是一個(gè)套路,基本上都在關(guān)注兩個(gè)點(diǎn)弛矛,什么是閉包够吩,閉包舉例,很有搬運(yùn)工的嫌疑丈氓。我看了這些文章之后,一個(gè)很大的感受是:如果讓我給別人講解閉包這個(gè)知識(shí)點(diǎn)强法,我能說得清楚嗎万俗?我的依據(jù)是什么?可信度有多大饮怯?我覺得我是懷疑我自己的闰歪,否定三連估計(jì)是妥了。
不同的階段做不同的事蓖墅,當(dāng)有一些基礎(chǔ)后库倘,我們還是可以適當(dāng)?shù)匮芯肯略恚灰≡趩栴}表面论矾!那么技術(shù)水平一般的我們教翩,應(yīng)該怎么辦,怎么從這些雜亂的文章中突圍贪壳?我覺得一個(gè)辦法是從一些比較權(quán)威的文檔上去找線索饱亿,比如ES規(guī)范,MDN闰靴,維基百科等彪笼。
關(guān)于閉包(closure),總是有著不同的解釋蚂且。
第一種說法是配猫,閉包是由函數(shù)以及聲明該函數(shù)的詞法環(huán)境組合而成的。這個(gè)說法來源于MDN-閉包杏死。
另外一種說法是泵肄,閉包是指有權(quán)訪問另外一個(gè)函數(shù)作用域中的變量的函數(shù)捆交。
從我的理解來看,我認(rèn)為第一個(gè)說法是正確的凡伊,閉包不是一個(gè)函數(shù)零渐,而是函數(shù)和詞法環(huán)境組成的。那么第二種說法對(duì)不對(duì)呢系忙?我覺得它說對(duì)了一半诵盼,在閉包場(chǎng)景下,確實(shí)存在一個(gè)函數(shù)有權(quán)訪問另外一個(gè)函數(shù)作用域中的變量银还,但閉包不是函數(shù)风宁。
這就完了嗎?顯然不是蛹疯!解讀閉包戒财,這次我們刨根究底(吹下牛逼)!
本文會(huì)直接從ECMAScript5規(guī)范入手解讀JS引擎的部分內(nèi)部實(shí)現(xiàn)邏輯捺弦,基于這些認(rèn)知再來重新審視閉包饮寞。
回到主題,上文提到的詞法環(huán)境(Lexical Environment)到底是什么列吼?
詞法環(huán)境
我們可以看看ES5規(guī)范第十章(可執(zhí)行代碼和執(zhí)行上下文)中的第二節(jié)詞法環(huán)境是怎么說的幽崩。
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code.
詞法環(huán)境是一種規(guī)范類型(specification type),它定義了標(biāo)識(shí)符和ECMAScript代碼中的特定變量及函數(shù)之間的聯(lián)系寞钥。
問題來了慌申,規(guī)范類型(specification type)又是什么?specification type是Type的一種理郑。從ES5規(guī)范中可以看到Type分為language types和specification types兩大類蹄溉。
language types是語言類型,我們熟知的類型您炉,也就是使用ECMAScript的程序員們可以操作的數(shù)據(jù)類型柒爵,包括Undefined
, Null
, Number
, String
, Boolean
和Object
。
而規(guī)范類型(specification type)是一種更抽象的元值(meta-values)邻吭,用于在算法中描述ECMAScript的語言結(jié)構(gòu)和語言類型的具體語義餐弱。
A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types.
至于元值是什么,我覺得可以理解為元數(shù)據(jù)囱晴,而元數(shù)據(jù)是什么意思膏蚓,可以簡(jiǎn)單看看這篇知乎什么是元數(shù)據(jù)?為何需要元數(shù)據(jù)畸写?
總的來說驮瞧,元數(shù)據(jù)是用來描述數(shù)據(jù)的數(shù)據(jù)。這一點(diǎn)就可以類比于枯芬,高級(jí)語言總要用一個(gè)更底層的語言和數(shù)據(jù)結(jié)構(gòu)來描述和表達(dá)论笔。這也就是JS引擎干的事情采郎。
大致理解了規(guī)范類型是什么后,我們不免要問下:規(guī)范類型(specification type)包含什么狂魔?
The specification types are Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, and Environment Record.
看到這里我好似明白了些什么蒜埋,原來詞法環(huán)境(Lexical Environment)和環(huán)境記錄(Environment Record)都是一種規(guī)范類型(specification type),果然是更底層的概念最楷。
先拋開List
, Completion
, Property Descriptor
, Property Identifier
等規(guī)范類型不說整份,我們接著看詞法環(huán)境(Lexical Environment)這種規(guī)范類型。
下面這句解釋了詞法環(huán)境到底包含了什么內(nèi)容:
A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
詞法環(huán)境包含了一個(gè)環(huán)境記錄(Environment Record)和一個(gè)指向外部詞法環(huán)境的引用籽孙,而這個(gè)引用的值可能為null烈评。
一個(gè)詞法環(huán)境的結(jié)構(gòu)如下:
Lexical Environment
+ Outer Reference
+ Environment Record
Outer Reference指向外部詞法環(huán)境,這也說明了詞法環(huán)境是一個(gè)鏈表結(jié)構(gòu)犯建。簡(jiǎn)單畫個(gè)結(jié)構(gòu)圖幫助理解下讲冠!
Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a WithStatement, or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated.
通常,詞法環(huán)境與ECMAScript代碼的某些特定語法結(jié)構(gòu)(如FunctionDeclaration适瓦,WithStatement或TryStatement的Catch子句)相關(guān)聯(lián)竿开,并且每次評(píng)估此類代碼時(shí)都會(huì)創(chuàng)建一個(gè)新的詞法環(huán)境。
PS:evaluated是evaluate的過去分詞玻熙,從字面上解釋就是評(píng)估德迹,而評(píng)估代碼我覺得不是很好理解。我個(gè)人的理解是揭芍,評(píng)估代碼代表著JS引擎在解釋執(zhí)行javascript代碼。
我們知道卸例,執(zhí)行函數(shù)會(huì)創(chuàng)建新的詞法環(huán)境称杨。
我們也認(rèn)同,with語句會(huì)“延長(zhǎng)”作用域(實(shí)際上是調(diào)用了NewObjectEnvironment筷转,創(chuàng)建了一個(gè)新的詞法環(huán)境姑原,詞法環(huán)境的環(huán)境記錄是一個(gè)對(duì)象環(huán)境記錄)。
以上這些是我們比較好理解的呜舒。那么catch子句對(duì)詞法環(huán)境做了什么锭汛?雖然try-catch平時(shí)用得還比較多,但是關(guān)于詞法環(huán)境的細(xì)節(jié)很多人都不會(huì)注意到袭蝗,包括我唤殴!
我們知道,catch子句會(huì)有一個(gè)錯(cuò)誤對(duì)象e
function test(value) {
var a = value;
try {
console.log(b);
// 直接引用一個(gè)不存在的變量到腥,會(huì)報(bào)ReferenceError
} catch(e) {
console.log(e, arguments, this)
}
}
test(1);
在catch
子句中打印arguments
朵逝,只是為了證明catch
子句不是一個(gè)函數(shù)。因?yàn)槿绻?code>catch是一個(gè)函數(shù)乡范,顯然這里打印的arguments
就不應(yīng)該是test
函數(shù)的arguments
配名。既然catch
不是一個(gè)函數(shù)啤咽,那么憑什么可以有一個(gè)僅限在catch
子句中被訪問的錯(cuò)誤對(duì)象e
?
答案就是catch
子句使用NewDeclarativeEnvironment創(chuàng)建了一個(gè)新的詞法環(huán)境(catch子句中詞法環(huán)境的外部詞法環(huán)境引用指向函數(shù)test的詞法環(huán)境)渠脉,然后通過CreateMutableBinding和SetMutableBinding將標(biāo)識(shí)符e與新的詞法環(huán)境的環(huán)境記錄關(guān)聯(lián)上宇整。
有人會(huì)說,for
循環(huán)中的initialization
部分也可以通過var
定義變量芋膘,和catch
子句有什么本質(zhì)區(qū)別嗎鳞青?要注意的是,在ES6之前是沒有塊級(jí)作用域的索赏。在for
循環(huán)中通過var
定義的變量原則上歸屬于所在函數(shù)的詞法環(huán)境盼玄。如果for
語句不是用在函數(shù)中,那么其中通過var
定義的變量就是屬于全局環(huán)境(The Global Environment)潜腻。
with語句和catch子句中建立了新的詞法環(huán)境這一結(jié)論埃儿,證據(jù)來源于上文中一句話“a new Lexical Environment is created each time such code is evaluated.”具體細(xì)節(jié)也可以看看12.10 The with Statement和12.14 The try Statement。
Environment Record
了解了詞法環(huán)境(Lexical Environment)融涣,接下來就說說詞法環(huán)境中的環(huán)境記錄(Environment Record)吧童番。環(huán)境記錄與我們使用的變量,函數(shù)息息相關(guān)威鹿,可以說環(huán)境記錄是它們的底層實(shí)現(xiàn)剃斧。
規(guī)范描述環(huán)境記錄的內(nèi)容太長(zhǎng),這兒就不全部復(fù)制了忽你,請(qǐng)直接打開ES5規(guī)范第10.2.1節(jié)閱讀幼东。
There are two kinds of Environment Record values used in this specification: declarative environment records and object environment records. // 省略一大段
從規(guī)范中我們可以看到環(huán)境記錄(Environment Record)分為兩種:
- declarative environment records 聲明式環(huán)境記錄
- object environment records 對(duì)象環(huán)境記錄
ECMAScript規(guī)范約束了聲明式環(huán)境記錄和對(duì)象環(huán)境記錄都必須實(shí)現(xiàn)環(huán)境記錄類的一些公共的抽象方法,即便他們?cè)诰唧w實(shí)現(xiàn)算法上可能不同科雳。
這些公共的抽象方法有:
- HasBinding(N)
- CreateMutableBinding(N, D)
- SetMutableBinding(N,V, S)
- GetBindingValue(N,S)
- DeleteBinding(N)
- ImplicitThisValue()
聲明式環(huán)境記錄還應(yīng)該實(shí)現(xiàn)兩個(gè)特有的方法:
- CreateImmutableBinding(N)
- InitializeImmutableBinding(N,V)
關(guān)于不可變綁定(ImmutableBinding)根蟹,在規(guī)范中有這么一段比較細(xì)致的場(chǎng)景描述:
If strict is true, then Call env’s CreateImmutableBinding concrete method passing the String "arguments" as the argument.
Call env’s InitializeImmutableBinding concrete method passing "arguments" and argsObj as arguments.
Else,Call env’s CreateMutableBinding concrete method passing the String "arguments" as the argument.
Call env’s SetMutableBinding concrete method passing "arguments", argsObj, and false as arguments.
也就是說蹄梢,只有嚴(yán)格模式下搔啊,才會(huì)對(duì)函數(shù)的arguments對(duì)象使用不可變綁定。應(yīng)用了不可變綁定(ImmutableBinding)的變量意味著不能再被重新賦值巨缘,舉個(gè)例子:
非嚴(yán)格模式下可以改變arguments的指向:
function test(a, b) {
arguments = [3, 4];
console.log(arguments, a, b)
}
test(1, 2)
// [3, 4] 1 2
而在嚴(yán)格模式下尿赚,改變arguments的指向會(huì)直接報(bào)錯(cuò):
"use strict";
function test(a, b) {
arguments = [3, 4];
console.log(arguments, a, b)
}
test(1, 2)
// Uncaught SyntaxError: Unexpected eval or arguments in strict mode
要注意散庶,我這里說的是改變arguments的指向,而不是修改arguments凌净。arguments[2] = 3
這種操作在嚴(yán)格模式下是不會(huì)報(bào)錯(cuò)的悲龟。
所以不可變綁定(ImmutableBinding)約束的是引用不可變,而不是約束引用指向的對(duì)象不可變泻蚊。
declarative environment records
在我們使用變量聲明躲舌,函數(shù)聲明,catch子句時(shí)性雄,就會(huì)在JS引擎中建立對(duì)應(yīng)的聲明式環(huán)境記錄没卸,它們直接將identifier bindings與ECMAScript的language values關(guān)聯(lián)到一起羹奉。
object environment records
對(duì)象環(huán)境記錄(object environment records),包含Program, WithStatement约计,以及后面說到的全局環(huán)境的環(huán)境記錄诀拭。它們將identifier bindings與某些對(duì)象的屬性關(guān)聯(lián)到一起。
看到這里煤蚌,我自己就想問下:identifier bindings是啥耕挨?
看了ES5規(guī)范中提到的環(huán)境記錄(Environment Record)的抽象方法后,我有了一個(gè)大致的答案尉桩。
先簡(jiǎn)單看一下javascript變量取值和賦值的過程:
var a = 1;
console.log(a);
我們?cè)诮o變量a
初始化并賦值1
的這樣一個(gè)步驟筒占,其實(shí)體現(xiàn)在JS引擎中,是執(zhí)行了CreateMutableBinding(創(chuàng)建可變綁定)和SetMutableBinding(設(shè)置可變綁定的值)蜘犁。
而在對(duì)變量a
取值時(shí)翰苫,體現(xiàn)在JS引擎中,是執(zhí)行了GetBindingValue(獲取綁定的值)这橙,這些執(zhí)行過程中會(huì)有一些斷言和判斷奏窑,也會(huì)牽涉到嚴(yán)格模式的判斷,具體見10.2.1.1 Declarative Environment Records屈扎。
這里也省略了一些步驟埃唯,比如說GetIdentifierReference, GetValue(V), PutValue(V) 等。
按我的理解鹰晨,identifier bindings就是JS引擎中維護(hù)的一組綁定關(guān)系墨叛,可以與javascript中的標(biāo)識(shí)符關(guān)聯(lián)起來。
The Global Environment
全局環(huán)境(The Global Environment)是一個(gè)特殊的詞法環(huán)境模蜡,在ECMAScript代碼執(zhí)行之前就被創(chuàng)建巍实。全局環(huán)境中的環(huán)境記錄(Environment Record)是一個(gè)對(duì)象環(huán)境記錄(object environment record),它被綁定到一個(gè)全局對(duì)象(Global Object)上哩牍,體現(xiàn)在瀏覽器環(huán)境中,與Global Object關(guān)聯(lián)的就是window對(duì)象令漂。
全局環(huán)境是一個(gè)頂層的詞法環(huán)境膝昆,因此全局環(huán)境不再有外部詞法環(huán)境,或者說它的外部詞法環(huán)境的引用是null叠必。
在15.1 The Global Object一節(jié)也解釋了Global Object的一些細(xì)節(jié)荚孵,比如為什么不能new Window()
,為什么在不同的宿主環(huán)境中全局對(duì)象會(huì)有很大區(qū)別......
執(zhí)行上下文
看了這些我們還是沒有一個(gè)全盤的把握去解讀閉包纬朝,不如接著看看執(zhí)行上下文收叶。在我之前的理解中,上下文應(yīng)該是一個(gè)環(huán)境共苛,包含了代碼可訪問的變量判没。當(dāng)然蜓萄,這顯然還不夠全面。那么上下文到底是什么澄峰?
When control is transferred to ECMAScript executable code, control is entering an execution context. Active execution contexts logically form a stack. The top execution context on this logical stack is the running execution context.
當(dāng)程序控制轉(zhuǎn)移到ECMAScript可執(zhí)行代碼(executable code)時(shí)嫉沽,就進(jìn)入了一個(gè)執(zhí)行上下文(execution context),執(zhí)行上下文是一個(gè)邏輯上的堆棧結(jié)構(gòu)(Stack)俏竞。堆棧中最頂層的執(zhí)行上下文就是正在運(yùn)行的執(zhí)行上下文绸硕。
很多人對(duì)可執(zhí)行代碼可能又有疑惑了,javascript不都是可執(zhí)行代碼嗎魂毁?不是的玻佩,比如注釋(Comment),空白符(White Space)就不是可執(zhí)行代碼席楚。
An execution context contains whatever state is necessary to track the execution progress of its associated code.
執(zhí)行上下文包含了一些狀態(tài)(state)咬崔,這些狀態(tài)用于跟蹤與之關(guān)聯(lián)的代碼的執(zhí)行進(jìn)程。每個(gè)執(zhí)行上下文都有這些狀態(tài)組件(Execution Context State Components)酣胀。
- LexicalEnvironment:詞法環(huán)境
- VariableEnvironment:變量環(huán)境
- ThisBinding:與執(zhí)行上下文直接關(guān)聯(lián)的this關(guān)鍵字
執(zhí)行上下文的創(chuàng)建
我們知道刁赦,解釋執(zhí)行g(shù)lobal code或使用eval function,調(diào)用函數(shù)都會(huì)創(chuàng)建一個(gè)新的執(zhí)行上下文闻镶,執(zhí)行上下文是堆棧結(jié)構(gòu)甚脉。
When control enters an execution context, the execution context’s ThisBinding is set, its VariableEnvironment and initial LexicalEnvironment are defined, and declaration binding instantiation (10.5) is performed. The exact manner in which these actions occur depend on the type of code being entered.
當(dāng)控制程序進(jìn)入執(zhí)行上下文時(shí),會(huì)發(fā)生下面這3個(gè)動(dòng)作:
- this關(guān)鍵字的值被設(shè)置铆农。
- 同時(shí)VariableEnvironment(不變的)和initial LexicalEnvironment(可能會(huì)變牺氨,所以這里說的是initial)被定義。
- 然后執(zhí)行聲明式綁定初始化操作墩剖。
以上這些動(dòng)作的執(zhí)行細(xì)節(jié)取決于代碼類型(分為global code, eval code, function code三類)猴凹。
PS:通常情況下,VariableEnvironment和LexicalEnvironment在初始化時(shí)是一致的岭皂,VariableEnvironment不會(huì)再發(fā)生變化郊霎,而LexicalEnvironment在代碼執(zhí)行的過程中可能會(huì)變化。
那么進(jìn)入global code爷绘,eval code书劝,function code時(shí),執(zhí)行上下文會(huì)發(fā)生什么不同的變化呢土至?感興趣的可以仔細(xì)閱讀下10.4 Establishing an Execution Context购对。
詞法環(huán)境的鏈表結(jié)構(gòu)
回顧一下上文,上文中提到陶因,詞法環(huán)境是一個(gè)鏈表結(jié)構(gòu)骡苞。
眾所周知,在理解閉包的時(shí)候,很多人都會(huì)提到作用域鏈(Scope Chain)這么一個(gè)概念解幽,同時(shí)會(huì)引出VO(變量對(duì)象)和AO(活動(dòng)對(duì)象)這些概念贴见。然而我在閱讀ECMAScript規(guī)范時(shí),通篇沒有找到這些關(guān)鍵詞亚铁。我就在想蝇刀,詞法環(huán)境的鏈表結(jié)構(gòu)是不是他們說的作用域鏈?VO徘溢,AO是不是已經(jīng)過時(shí)的概念吞琐?但是這些概念又好像成了“權(quán)威”,一搜相關(guān)的文章然爆,都在說VO, AO站粟,我真的也要這樣去理解嗎?
在ECMAScript中曾雕,找到8.6.2 Object Internal Properties and Methods一節(jié)中的Table 9 Internal Properties Only Defined for Some Objects奴烙,的確存在[[Scope]]這么一個(gè)內(nèi)部屬性,按照Scope單詞的意思剖张,[[Scope]]不就是函數(shù)作用域嘛切诀!
在這個(gè)Table中,我們可以明確看到[[Scope]]的Value Type Domain一列的值是Lexical Environment搔弄,這說明[[Scope]]就是一種詞法環(huán)境幅虑。我們接著看看Description:
A lexical environment that defines the environment in which a Function object is executed. Of the standard built-in ECMAScript objects, only Function objects implement [[Scope]].
仔細(xì)看下,[[Scope]]是函數(shù)對(duì)象被執(zhí)行時(shí)所在的環(huán)境顾犹,而且只有函數(shù)實(shí)現(xiàn)了[[Scope]]屬性倒庵,這意味著[[Scope]]是函數(shù)特有的屬性。
所以炫刷,我是不是可以理解為:作用域鏈(Scope Chain)就是函數(shù)執(zhí)行時(shí)能訪問的詞法環(huán)境鏈擎宝。而廣義上的詞法環(huán)境鏈表不僅包含了作用域鏈,還包括WithStatement和Catch子句中的詞法環(huán)境浑玛,甚至包含ES6的Block-Level詞法環(huán)境绍申。這么看來,ECMAScript是非常嚴(yán)謹(jǐn)?shù)模?/p>
而VO顾彰,AO這兩個(gè)相對(duì)陳舊的概念失晴,由于沒有官方的解釋,所以基本上是“一千個(gè)讀者拘央,一千個(gè)哈姆雷特”了,我覺得可能這樣理解也行:
- VO是詞法分析(Lexical Parsing)階段的產(chǎn)物
- AO是代碼執(zhí)行(Execution)階段的產(chǎn)物
ES5及ES6規(guī)范中是沒有這樣的字眼的书在,所以干脆忘掉VO, AO吧灰伟!
閉包
什么是閉包?
文章最開始提到了閉包是由函數(shù)和詞法環(huán)境組成。這里再引用一段維基百科的閉包解釋佐證下栏账。
在計(jì)算機(jī)科學(xué)中帖族,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures)挡爵,是在支持頭等函數(shù)的編程語言中實(shí)現(xiàn)詞法綁定的一種技術(shù)竖般。閉包在實(shí)現(xiàn)上是一個(gè)結(jié)構(gòu)體,它存儲(chǔ)了一個(gè)函數(shù)(通常是其入口地址)和一個(gè)關(guān)聯(lián)的環(huán)境(相當(dāng)于一個(gè)符號(hào)查找表)茶鹃。環(huán)境里是若干對(duì)符號(hào)和值的對(duì)應(yīng)關(guān)系涣雕,它既要包括約束變量(該函數(shù)內(nèi)部綁定的符號(hào)),也要包括自由變量(在函數(shù)外部定義但在函數(shù)內(nèi)被引用)闭翩,有些函數(shù)也可能沒有自由變量挣郭。閉包跟函數(shù)最大的不同在于,當(dāng)捕捉閉包的時(shí)候疗韵,它的自由變量會(huì)在捕捉時(shí)被確定兑障,這樣即便脫離了捕捉時(shí)的上下文,它也能照常運(yùn)行蕉汪。
這是站在計(jì)算機(jī)科學(xué)的角度解釋什么是閉包流译,當(dāng)然這同樣適用于javascript!
里面提到了一個(gè)詞“自由變量”者疤,也就是閉包詞法環(huán)境中我們重點(diǎn)關(guān)注的變量福澡。
Chrome如何定義閉包?
Chrome瀏覽器似乎已經(jīng)成為了前端的標(biāo)準(zhǔn)宛渐,那么在Chrome瀏覽器中竞漾,是如何判定閉包的呢?不妨來探索下窥翩!
function test() {
var a = 1;
function increase() {
debugger;
var b = 2;
a++;
return a;
};
increase();
}
test();
我把debugger置于內(nèi)部函數(shù)increase
中业岁,調(diào)試時(shí)我們直接看右側(cè)的高亮部分,可以發(fā)現(xiàn)寇蚊,Scope中存在一個(gè)Closure(閉包)笔时,Closure的名稱是外部函數(shù)test
的函數(shù)名,閉包中的變量a
是在函數(shù)test
中定義的仗岸,而變量b
是作為本地變量處于Local
中允耿。
PS: 關(guān)于本地變量,可以參見localEnv扒怖。
假設(shè)我在外部函數(shù)test
中再定義一個(gè)變量c
较锡,但是在內(nèi)部函數(shù)increase
中不引用它,會(huì)怎么樣呢盗痒?
function test() {
var a = 1;
var c = 3; // c不在閉包中
function increase() {
debugger;
var b = 2;
a++;
return a;
};
increase();
}
test();
經(jīng)驗(yàn)證蚂蕴,內(nèi)部函數(shù)increase
執(zhí)行時(shí)低散,變量c
沒有在閉包中。
我們還可以驗(yàn)證骡楼,如果內(nèi)部函數(shù)increase
不引用任何外部函數(shù)test
中的變量熔号,就不會(huì)產(chǎn)生閉包。
所以到這里鸟整,我們可以下這樣一個(gè)結(jié)論引镊,閉包產(chǎn)生的必要條件是:
- 存在函數(shù)嵌套;
- 嵌套的內(nèi)部函數(shù)必須引用在外部函數(shù)中定義的變量篮条;
- 嵌套的內(nèi)部函數(shù)必須被執(zhí)行弟头。
面試官最喜歡問的閉包
在面試過程中,我們通常被問到的閉包場(chǎng)景是:內(nèi)部函數(shù)引用了外部函數(shù)的變量兑燥,并且作為外部函數(shù)的返回值亮瓷。這是一種特殊的閉包,舉個(gè)例子看下:
function test() {
var a = 1;
function increase() {
a++;
};
function getValue() {
return a;
}
return {
increase,
getValue
}
}
var adder = test();
adder.increase(); // 自增1
adder.getValue(); // 2
adder.increase();
adder.getValue(); // 3
在這個(gè)例子中降瞳,我們發(fā)現(xiàn)嘱支,每調(diào)用一次adder.increase()
方法后,a
的值會(huì)就會(huì)比上一次增加1
挣饥,也就是說除师,變量a
被保持在內(nèi)存中沒有被釋放。
那么這種現(xiàn)象背后到底是怎么回事呢扔枫?
閉包分析
既然閉包涉及到內(nèi)存問題汛聚,那么不得不提一嘴V8的GC(垃圾回收)機(jī)制。
我們從書本上了解最多的GC策略就是引用計(jì)數(shù)短荐,但是現(xiàn)代主流VM(包括V8, JVM等)都不采用引用計(jì)數(shù)的回收策略倚舀,而是采用可達(dá)性算法。
引用計(jì)數(shù)讓人比較容易理解忍宋,所以常見于教材中痕貌,但是可能存在對(duì)象相互引用而無法釋放其內(nèi)存的問題。而可達(dá)性算法是從GC Roots對(duì)象(比如全局對(duì)象window)開始進(jìn)行搜索存活(可達(dá))對(duì)象糠排,不可達(dá)對(duì)象會(huì)被回收舵稠,存活對(duì)象會(huì)經(jīng)歷一系列的處理。
關(guān)于V8 GC的一些算法細(xì)節(jié)入宦,有一篇文章講得特別好哺徊,作者是洗影,非常建議去看看乾闰,已附在文末的參考資料中落追。
而在我們關(guān)注的這種特殊閉包場(chǎng)景下,之所以閉包變量會(huì)保持在內(nèi)存中涯肩,是因?yàn)殚]包的詞法環(huán)境沒有被釋放轿钠。我們先來分析下執(zhí)行過程雹熬。
function test() {
var a = 1;
function increase() {
a++;
};
function getValue() {
return a;
}
return {
increase,
getValue
}
}
var adder = test();
adder.increase();
adder.getValue();
- 初始執(zhí)行g(shù)lobal code,創(chuàng)建全局執(zhí)行上下文谣膳,隨之設(shè)置
this
關(guān)鍵詞的值為window
對(duì)象,創(chuàng)建全局環(huán)境(Global Environment)铅乡。全局對(duì)象下有adder
,test
等變量和函數(shù)聲明继谚。
- 開始執(zhí)行
test
函數(shù),進(jìn)入test
函數(shù)執(zhí)行上下文阵幸。在test
函數(shù)執(zhí)行過程中花履,聲明了變量a
,函數(shù)increase
和getValue
挚赊。最終返回一個(gè)對(duì)象诡壁,該對(duì)象的兩個(gè)屬性分別引用了函數(shù)increase
和getValue
。
- 退出
test
函數(shù)執(zhí)行上下文荠割,test
函數(shù)的執(zhí)行結(jié)果賦值給變量adder
妹卿,當(dāng)前執(zhí)行上下文恢復(fù)成全局執(zhí)行上下文。
- 調(diào)用
adder
的increase
方法蔑鹦,進(jìn)入increase
函數(shù)的執(zhí)行上下文夺克,執(zhí)行代碼使變量a
自增1
。
- 退出
increase
函數(shù)的執(zhí)行上下文嚎朽。 - 調(diào)用
adder
的getValue
方法铺纽,其過程與調(diào)用increase
方法的過程類似。
對(duì)整個(gè)執(zhí)行過程有了一定認(rèn)識(shí)后哟忍,我們似乎也很難解釋為什么閉包中的變量a
不會(huì)被GC回收狡门。只有一個(gè)事實(shí)是很清楚的,那就是每次執(zhí)行increase
和getValue
方法時(shí)锅很,都依賴函數(shù)test
中定義的變量a
其馏,但僅憑這個(gè)事實(shí)作為理由顯然也是不具有說服力。
這里不妨拋出一個(gè)問題粗蔚,代碼是如何解析a
這個(gè)標(biāo)識(shí)符的呢尝偎?
通過閱讀規(guī)范,我們可以知道鹏控,解析標(biāo)識(shí)符是通過GetIdentifierReference(lex, name, strict)
致扯,其中lex
是詞法環(huán)境,name
是標(biāo)識(shí)符名稱当辐,strict
是嚴(yán)格模式的布爾型標(biāo)志抖僵。
那么在執(zhí)行函數(shù)increase
時(shí),是怎么解析標(biāo)識(shí)符a
的呢缘揪?我們來分析下耍群!
- 首先义桂,讓
lex
的值為函數(shù)increase
的localEnv
(函數(shù)的本地環(huán)境),通過GetIdentifierReference(lex, name, strict)
在localEnv
中解析標(biāo)識(shí)符a
蹈垢。 - 根據(jù)GetIdentifierReference的執(zhí)行邏輯慷吊,在
localEnv
并不能解析到標(biāo)識(shí)符a
(因?yàn)?code>a不是在函數(shù)increase
中聲明的,這很明顯)曹抬,所以會(huì)轉(zhuǎn)到localEnv
的外部詞法環(huán)境繼續(xù)查找溉瓶,而這個(gè)外部詞法環(huán)境其實(shí)就是increase
函數(shù)的內(nèi)部屬性[[Scope]](這一點(diǎn)我是從仔細(xì)看了多遍規(guī)范定義得出的),也就是test
函數(shù)的localEnv
的“閹割版”谤民。 - 回到執(zhí)行函數(shù)
test
那一步堰酿,執(zhí)行完函數(shù)test
后,函數(shù)test
中localEnv
中的其他變量的binding都能在后續(xù)GC的過程中被釋放张足,唯獨(dú)a
的binding不能被釋放触创,因?yàn)檫€有其他詞法環(huán)境(increase
函數(shù)的內(nèi)部屬性[[Scope]])會(huì)引用a
。 - 閉包的詞法環(huán)境和函數(shù)
test
執(zhí)行時(shí)的localEnv
是不一樣的为牍。函數(shù)test
執(zhí)行時(shí)哼绑,其localEnv
會(huì)完完整整地重新初始化一遍,而退出函數(shù)test
的執(zhí)行上下文后吵聪,閉包詞法環(huán)境只保留了其環(huán)境記錄中的一部分bindings凌那,這部分bindings會(huì)被其他詞法環(huán)境引用,所以我稱之為“閹割版”吟逝。
這里可能會(huì)有朋友提出一個(gè)疑問(我也這樣問過我自己)帽蝶,為什么adder.increase()
是在全局執(zhí)行上下文中被調(diào)用,它執(zhí)行時(shí)的外部詞法環(huán)境仍然是test
函數(shù)的localEnv
的“閹割版”块攒?
這就要回到外部詞法環(huán)境引用的定義了励稳,外部詞法環(huán)境引用指向的是邏輯上包圍內(nèi)部詞法環(huán)境的詞法環(huán)境!
The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment.
閉包的優(yōu)缺點(diǎn)
網(wǎng)上的文章關(guān)于這一塊還是講得挺詳細(xì)的囱井,本文就不再舉例了驹尼。總的來說庞呕,閉包有這么一些優(yōu)點(diǎn):
- 變量常駐內(nèi)存新翎,對(duì)于實(shí)現(xiàn)某些業(yè)務(wù)很有幫助,比如計(jì)數(shù)器之類的住练。
- 架起了一座橋梁地啰,讓函數(shù)外部訪問函數(shù)內(nèi)部變量成為可能。
- 私有化讲逛,一定程序上解決命名沖突問題亏吝,可以實(shí)現(xiàn)私有變量。
閉包是雙刃劍盏混,也存在這么一個(gè)比較明顯的缺點(diǎn):
- 存在這樣的可能蔚鸥,變量常駐在內(nèi)存中惜论,其占用內(nèi)存無法被GC回收,導(dǎo)致內(nèi)存溢出止喷。
小結(jié)
本文從ECMAScript規(guī)范入手馆类,一步一步揭開了閉包的神秘面紗。首先從閉包的定義了解到詞法環(huán)境弹谁,從詞法環(huán)境又引出環(huán)境記錄蹦掐,外部詞法環(huán)境引用和執(zhí)行上下文等概念。在對(duì)VO, AO等舊概念產(chǎn)生懷疑后僵闯,我選擇了從規(guī)范中尋找線索,最終有了頭緒藤滥。解讀閉包時(shí)鳖粟,我尋找了多方資料,從計(jì)算機(jī)科學(xué)的閉包通用定義入手拙绊,將一些關(guān)鍵概念映射到j(luò)avascript中向图,結(jié)合GC的一些知識(shí)點(diǎn),算是有了答案标沪。
寫這篇文章花了不少時(shí)間榄攀,因?yàn)樯婕暗紼CMAScript規(guī)范,一些描述必須客觀嚴(yán)謹(jǐn)金句。解讀過程必然存在主觀成分檩赢,如有錯(cuò)誤之處,還望指出违寞!
最后贞瞒,非常建議大家在有空的時(shí)候多多閱讀ECMAScript規(guī)范。閱讀語言規(guī)范是一個(gè)很好的解惑方式趁曼,能讓我們更好地理解一門語言的基本原理军浆。就比如假設(shè)我們不清楚某個(gè)運(yùn)算符的執(zhí)行邏輯,那么直接看語言規(guī)范是最穩(wěn)妥的挡闰!
結(jié)尾附上一張可以幫助你理解ECMAScript規(guī)范的圖片乒融。
如果方便的話,幫我點(diǎn)個(gè)贊喲摄悯,謝謝赞季!歡迎加我微信laobaife
交流,技術(shù)會(huì)友射众,閑聊亦可碟摆。