(注1:如果有問題歡迎留言探討,一起學習!轉載請注明出處,喜歡可以點個贊哦?。?br> (注2:更多內(nèi)容請查看我的目錄。)
1. 簡介
閉包,是讓很多JS初學者聞之色變的一個概念。每次看過一些書籍或者網(wǎng)上的例子,會感覺自己懂了很多,但又是似懂非懂。這篇文章,我們會結合前面所學,深入探討一下閉包的原理,讓大家從根本上弄明白閉包產(chǎn)生的原因。
2. 定義
關于閉包的定義,是讓大家迷惑的第一個點。因為不同書籍,不同的大神對閉包的解讀和定義不盡相同。
2.1. 定義一
我們先來看一下《JavaScript高級程序設計》一書中對閉包的的定義:
閉包是指有權訪問另一個函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見方式,就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù)。
再來看一下百度百科(百度百科-閉包)中對閉包的定義:
閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。由于在javascript中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,所以閉包可以理解成“定義在一個函數(shù)內(nèi)部的函數(shù)“。在本質(zhì)上,閉包是將函數(shù)內(nèi)部和函數(shù)外部連接起來的橋梁。
可以看到,百度百科和《JavaScript高級程序設計》一書對閉包的定義基本相同,即“定義在一個函數(shù)內(nèi)部的函數(shù)”。這是目前比較普遍被接受的一個定義。
2.2 定義二
看一下MDN對閉包的定義:
閉包是一個函數(shù)和聲明該函數(shù)的詞法環(huán)境的組合。從理論角度來說,所有函數(shù)都是閉包。
該說法認為“所有函數(shù)都是閉包”,是一個很寬泛的概念。
2.3 我對閉包的理解定義
其實,網(wǎng)上還有許多關于閉包的定義。說法各不相同,有從函數(shù)定義的角度出發(fā),有從使用的角度出發(fā),眾說紛紜,讓人無所適從??墒俏以诖酥涣信e了以上兩種定義,為什么呢?是對其余的定義不認同嗎?并不是。是因為,對于閉包來講,我們關注的并不是其定義或者概念,而是一種現(xiàn)象和產(chǎn)生這種現(xiàn)象的機制原理。那就是一個函數(shù)被嵌套時,不管在哪里被調(diào)用,為什么總能訪問其外層嵌套函數(shù)作用域的變量?這么說,可能有人不理解,我們舉一個簡單的例子:
全局A定義函數(shù)B,函數(shù)B嵌套函數(shù)C,函數(shù)C嵌套函數(shù)D,函數(shù)D在C直接執(zhí)行,或者通過賦值或者返回,在B,A任何一個作用域內(nèi)被引用,當其執(zhí)行時,都可以訪問C,B,A作用域內(nèi)定義的變量。(當然,A作為全局作用域,只要程序未銷毀,其中定義的變量始終是可以被訪問的,所以不做討論。)
所以,非要讓我對閉包下一個定義,我是傾向于定義一的。這其實是分了兩個情況:
- 被嵌套函數(shù)只在當前作用域。
- 被嵌套函數(shù)在非當前作用域被引用。
對應了兩個使用場景:
- 被嵌套函數(shù)只在當前作用域執(zhí)行。
- 被嵌套函數(shù)在非當前作用域被執(zhí)行。
下面,我們來分析一下這兩種情況的深層原因。
3. 深入解析閉包
3.1 被嵌套函數(shù)只在當前作用域
對于這種情況,其實在前面文章中我們已經(jīng)做了很詳盡的解釋。之所以被嵌套函數(shù)有權訪問其外層嵌套函數(shù)作用域中的變量,是因為作用域鏈的原因。
看下面這段代碼:
function foo() {
var a = 2;
function f() {
console.log(a); // 2
}
f();
}
foo();
為什么在f的內(nèi)部能訪問foo作用域的變量a呢?我們講過,在f執(zhí)行時,創(chuàng)建f的執(zhí)行上下文。此時:
fooContext = {
AO = {
arguments: {
length: 0
},
a: 2
},
Scope: [AO, globalContext.VO],
this: undefined
};
fContext = {
AO = {
arguments: {
length: 0
}
},
Scope: [AO, fooContext.AO, globalContext.VO],
this: undefined
};
此時,執(zhí)行
console.log(a);
會對a進行RHS查找,查找過程是沿著作用域鏈向上,此時在fooContext.AO找到了a的值為2。
3.2 被嵌套函數(shù)在非當前作用域被引用。
發(fā)生這種情況,可能是被嵌套函數(shù)被當做返回值返回,也可能是直接賦值給了外部的變量。我們來看一下這兩種情況。
3.2.1 被嵌套函數(shù)被當做返回值返回
看如下這段代碼:
function foo() {
var a = 2;
function f() {
console.log(a); // 2
}
return f;
}
foo()();
此處的關鍵節(jié)點在于foo的執(zhí)行結果。我們在return f;處打一個斷點,拍一張此時的快照:
ECStack = [
fooContext,
globalContext
];
fooContext = {
AO = {
arguments: {
length: 0
},
a: 2
},
Scope: [AO, globalContext.VO],
this: undefined
};
f.[[scope]] = fooContext.Scope = [fooContext.AO, globalContext.VO];
然后我們在foo()()處打一個斷點,此時foo()已經(jīng)執(zhí)行完畢,返回了f,準備執(zhí)行foo()(),即f()。
ECStack = [
globalContext
];
f.[[scope]] = fooContext.Scope = [fooContext.AO, globalContext.VO];
此時,fooContext出棧并銷毀。但是,關鍵的一點請注意,雖然foo的執(zhí)行上下文fooContext銷毀了,也不在對其活動對象fooContext.AO有引用了。但是,f.[[scope]]仍然保留有對fooContext.AO的引用,所以,fooContext.AO并沒有被銷毀,仍然存在于內(nèi)存中。然后我們再往下走,執(zhí)行f()
fContext = {
AO = {
arguments: {
length: 0
}
},
Scope: [AO, fooContext.AO, globalContext.VO],
this: undefined
}
fContext利用f的[[scope]]信息生成了作用域鏈Scope: [AO, fooContext.AO, globalContext.VO]。此時執(zhí)行
console.log(a);
的過程與3.1示例相同。
3.2.2 直接賦值給了外部的變量
看下面代碼:
var bar;
function foo(){
var a = 2;
function f(){
console.log(a); // 2
}
bar = f;
}
foo();
bar();
其實這種情況,和3.2.1的執(zhí)行原理是一樣的。都是因為bar中保留有對foo的執(zhí)行環(huán)境的活動對象的引用。具體的分析過程,大家可以自己嘗試一下。
4. 總結
通過這篇文章的分析,我們可以清晰的看到閉包形成的原理,也可以看出閉包為什么可能造成內(nèi)存被大量占用的原因。這里需要注意的是,只要被嵌套函數(shù)被返回或者賦值給了作用域以外的地方,那么在其所有引用執(zhí)行完畢前,都會造成對其包含函數(shù)執(zhí)行環(huán)境的活動對象的持續(xù)引用。
參考
深入理解閉包系列第一篇——到底什么才是閉包
深入理解閉包系列第二篇——從執(zhí)行環(huán)境角度看閉包
JavaScript深入之閉包
百度百科-閉包
BOOK-《JavaScript高級程序設計(第3版)》第7章