前言
閉包是JS
中重要的內(nèi)容洪鸭,對大多數(shù)人來說都會(huì)覺的閉包本身很好理解民珍,不就是一個(gè)函數(shù)嵌套一個(gè)函數(shù)嗎?但是再深入解釋時(shí)放航,好像不知道要說些啥烈拒。不用擔(dān)心,相信看完這篇你對閉包的理解就不僅僅只停留在概念層面上了三椿。
基本概念
1缺菌、閉包是什么?
官方對閉包的解釋是:一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式(通常是一個(gè)函數(shù))搜锰,因而這些變量也是該表達(dá)式的一部分。
通俗的解釋是:閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)耿战。
更清晰的講:閉包就是一個(gè)函數(shù)蛋叼,這個(gè)函數(shù)能夠訪問其他函數(shù)的作用域中的變量。
JS為什么使用閉包剂陡?
因?yàn)?code>JS中變量的作用域分為全局變量和局部變量狈涮。在函數(shù)外部無法讀取函數(shù)內(nèi)的局部變量。需要閉包來解決鸭栖。
閉包帶來的問題歌馍?
濫用閉包,會(huì)造成內(nèi)存泄漏晕鹊;由于閉包會(huì)使得函數(shù)中的變量都被保存在內(nèi)存中松却,內(nèi)存消耗很大暴浦,所以不能濫用閉包,否則會(huì)造成網(wǎng)頁的性能問題晓锻,在IE
中可能導(dǎo)致內(nèi)存泄露歌焦。解決方法是,在退出函數(shù)之前砚哆,將不使用的局部變量指向null
独撇。
閉包相關(guān)概念
在了解閉包之前,先來了解作用域躁锁、執(zhí)行上下文纷铣、變量對象、活動(dòng)對象战转、作用域鏈关炼,這些將有助于對閉包的理解。
作用域(Scope)
作用域是一套規(guī)則匣吊,用于確定在何處以及如何查找變量(標(biāo)識符)儒拂。
1、作用域分為
- 全局作用域
- 函數(shù)作用域(ES6新增了塊級作用域)
PS:這些相信很多人都知道色鸳,就不詳細(xì)舉例了
2社痛、作用域共有兩種主要的工作模型
- 詞法(靜態(tài))作用域:作用域是在編寫代碼的時(shí)候確定的
- 動(dòng)態(tài)作用域:作用域是在代碼運(yùn)行的時(shí)候確定的
我們知道Javascript
使用的是詞法(靜態(tài))作用域
。
3命雀、詞法(靜態(tài))作用域
理解靜態(tài)作用域之前蒜哀,首先要先了解Js在編譯階段做了些什么事情。
Js編譯階段分為三個(gè)階段吏砂,下面概括一下三個(gè)階段:
1. 分詞/詞法分析(Tokenizing/Lexing)
其實(shí)我們寫的代碼就是字符串撵儿,在編譯的第一個(gè)階段里,把這些字符串轉(zhuǎn)成詞法單元(toekn)狐血。
2. 解析/語法分析(Parsing)
在有了詞法單元之后淀歇,JS
還需要繼續(xù)分解代碼中的語法以便為JS
引擎減輕負(fù)擔(dān),通過詞法單元生成了一個(gè)抽象語法樹(Abstract Syntax Tree),它的作用是為JS
引擎構(gòu)造出一份程序語法樹匈织,我們簡稱為AST
浪默。
3. 代碼生成(raw code)
這個(gè)階段主要做的就是拿AST
來生成一份JS
語言內(nèi)部認(rèn)可的代碼。
靜態(tài)作用域是發(fā)生在編譯階段的第一個(gè)步驟當(dāng)中缀匕,也就是分詞/詞法分析階段纳决。它有兩種可能,分詞和詞法分析乡小,分詞是無狀態(tài)的阔加,而詞法分析是有狀態(tài)的。
那我們?nèi)绾闻袛嘤袩o狀態(tài)呢满钟?以var a = 1
為例胜榔。
- 如果詞法單元生成器在判斷
a
是否為一個(gè)獨(dú)立的詞法單元時(shí)胳喷,調(diào)用的是有狀態(tài)的解析規(guī)則(生成器不清楚它是否依賴于其他詞法單元,所以要進(jìn)一步解析)苗分。 - 如果它不用生成器判斷厌蔽,是一條不用被賦予語意的代碼(暫時(shí)可以理解為不涉及作用域的代碼,因?yàn)閖s內(nèi)部定義什么樣的規(guī)則我們并不清楚),那就被列入分詞中了摔癣。
總的來說奴饮,如果詞法單元生成器拿不準(zhǔn)當(dāng)前詞法單元是否為獨(dú)立的,就進(jìn)入詞法分析择浊,否則就進(jìn)入分詞階段戴卜。
簡單的說,詞法作用域就是定義在詞法階段的作用域琢岩。詞法作用域就是你編寫代碼時(shí)投剥,變量和塊級作用域?qū)懺谀睦餂Q定的。當(dāng)詞法解析器處理代碼時(shí)担孔,會(huì)保持作用域不變(除動(dòng)態(tài)作用域)江锨。
執(zhí)行上下文(Execution Contexts)
1、Javascript
中代碼的執(zhí)行上下文分為以下三種:
- 全局級別的代碼 – 這個(gè)是默認(rèn)的代碼運(yùn)行環(huán)境糕篇,一旦代碼被載入啄育,引擎最先進(jìn)入的就是這個(gè)環(huán)境。
- 函數(shù)級別的代碼 – 當(dāng)執(zhí)行一個(gè)函數(shù)時(shí)拌消,運(yùn)行函數(shù)體中的代碼挑豌。
-
Eval
的代碼 – 在Eval
函數(shù)內(nèi)運(yùn)行的代碼。
2墩崩、一個(gè)執(zhí)行的上下文可以抽象的理解為一個(gè)對象氓英。每一個(gè)執(zhí)行的上下文都有一系列的屬性:
- 變量對象(variable object)
- this指針(this value)
- 作用域鏈(scope chain)
代碼表示如下:
Execution Contexts = {
variable object:變量對象/活動(dòng)對象;
this value: this指針;
scope chain:作用域鏈;
}
3、如何管理執(zhí)行上下文
Js
的運(yùn)行采用棧
的方式對執(zhí)行上下文進(jìn)行管理鹦筹,棧底始終是全局上下文铝阐,棧頂始終是正在被調(diào)用執(zhí)行的函數(shù)的執(zhí)行上下文。
實(shí)例
var a = 10;
var b = 'hello';
function fun1 () {
console.log('i am fun1...');
fun2();
}
function fun2 () {
console.log('i am fun2...');
}
fun1();
上面代碼具體流程是:
- 當(dāng)
Js
文件開始執(zhí)行時(shí)盛龄,創(chuàng)建全局上下文饰迹,并push
到call stack
。 -
fun1()
被調(diào)用時(shí)余舶,創(chuàng)建fun1
上下文,push
到call stack
锹淌。 -
fun2()
被調(diào)用時(shí)匿值,創(chuàng)建fun2
上下文,push
到call stack
赂摆。 -
fun2()
執(zhí)行完畢挟憔,fun2
上下文pop
出棧钟些,等待被回收。 -
fun1()
執(zhí)行完畢绊谭,fun1
上下文pop
出棧政恍,等待被回收。 - 全局執(zhí)行環(huán)境不會(huì)出棧达传。
4篙耗、執(zhí)行環(huán)境的生命周期
執(zhí)行上下文生命周期分為創(chuàng)建階段、執(zhí)行階段宪赶、執(zhí)行完畢宗弯。如下圖:
執(zhí)行上下文是代碼執(zhí)行的一種抽象,而代碼執(zhí)行除了整個(gè)Js開始執(zhí)行之外搂妻,代碼的執(zhí)行都是通過函數(shù)調(diào)用執(zhí)行的蒙保,所以執(zhí)行上下文生命周期的各個(gè)階段其實(shí)是可以分別對應(yīng)函數(shù)被調(diào)用時(shí)的初始化、執(zhí)行欲主、執(zhí)行完畢階段的邓厕。下面會(huì)詳細(xì)的解釋每個(gè)階段的過程。
變量對象(variable object)
1扁瓢、變量對象的定義:
如果變量與執(zhí)行上下文相關(guān)详恼,那變量自己應(yīng)該知道它的數(shù)據(jù)存儲(chǔ)在哪里,并且知道如何訪問涤妒。這種機(jī)制稱為變量對象(variable object)单雾。
2、變量對象的作用:
可以說變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域(scope of data) 她紫。它是與執(zhí)行上下文關(guān)聯(lián)的特殊對象硅堆,用于存儲(chǔ)被定義在執(zhí)行上下文中的變量(variables)、函數(shù)聲明(function declarations) 贿讹、arguments渐逃。
3、變量對象的創(chuàng)建過程:
實(shí)例
function add(num){
var sum = 5;
return sum + num;
}
var sum = add(4);
根據(jù)上面代碼民褂,創(chuàng)建變量對象的流程是:
- 檢查當(dāng)前執(zhí)行環(huán)境上的參數(shù)列表茄菊,建立
Arguments
對象,并作為add
VO
的arguments
屬性值赊堪。 - 檢查當(dāng)前執(zhí)行環(huán)境上的
function
函數(shù)聲明面殖,每檢查到一個(gè)函數(shù)聲明,就在變量對象中以函數(shù)名建立一個(gè)屬性哭廉,屬性指向函數(shù)所在的內(nèi)存地址脊僚。 - 檢查當(dāng)前執(zhí)行環(huán)境上的所有var變量聲明。每檢查到一個(gè)
var
聲明遵绰,如果VO
中已存在function
屬性名則跳過辽幌,如果沒有就在變量對象中以變量名新建一個(gè)屬性增淹,屬性值為undefined
。
當(dāng)進(jìn)入全局上下文時(shí)乌企,全局上下文的變量對象可表示為:
VO = {
add: <reference to function>,
sum: undefined,
Math: <...>,
String: <...>
...
window: global //引用自身
}
活動(dòng)對象(Activation Object)
當(dāng)函數(shù)被調(diào)用者激活時(shí)虑润,這個(gè)特殊的活動(dòng)對象(activation object) 就被創(chuàng)建了。它包含普通參數(shù)(formal parameters) 與特殊參數(shù)(arguments)對象(具有索引屬性的參數(shù)映射表)加酵∪鳎活動(dòng)對象在函數(shù)上下文中作為變量對象使用。
根據(jù)上圖虽画,簡單解釋:在沒有執(zhí)行當(dāng)前環(huán)境之前舞蔽,變量對象中的屬性都不能訪問!但是進(jìn)入執(zhí)行階段之后码撰,變量對象轉(zhuǎn)變?yōu)榱嘶顒?dòng)對象渗柿,里面的屬性都能被訪問了,然后開始進(jìn)行執(zhí)行階段的操作脖岛。所以活動(dòng)對象實(shí)際就是變量對象在真正執(zhí)行時(shí)的另一種形式朵栖。
根據(jù)上面變量對象的實(shí)例。當(dāng)add
函數(shù)被調(diào)用時(shí)柴梆,add
函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端陨溅,add
函數(shù)執(zhí)行上下文中活動(dòng)對象可表示為
AO = {
num: 4,
sum: 5,
arguments:{0:4}
}
作用域鏈 (Scope Chain)
函數(shù)上下文的作用域鏈在函數(shù)調(diào)用時(shí)創(chuàng)建的,包含活動(dòng)對象AO
和這個(gè)函數(shù)內(nèi)部的[[scope]]
屬性绍在。
實(shí)例
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo();
在這段代碼中我們看到變量y
在函數(shù)foo
中定義(意味著它在foo
上下文的AO
中)z
在函數(shù)bar
中定義门扇,但是變量x
并未在bar
上下文中定義,相應(yīng)地偿渡,它也不會(huì)添加到bar
的AO
中臼寄。乍一看,變量x
相對于函數(shù)bar
根本就不存在溜宽。
函數(shù)bar
如何訪問到變量x
吉拳?理論上函數(shù)應(yīng)該能訪問一個(gè)更高一層上下文的變量對象。實(shí)際上它正是這樣适揉,這種機(jī)制是通過函數(shù)內(nèi)部的[[scope]]
屬性來實(shí)現(xiàn)的留攒。
[[scope]]
是所有父級變量對象的層級鏈,處于當(dāng)前函數(shù)上下文之上嫉嘀,在函數(shù)創(chuàng)建時(shí)存于其中炼邀。
根據(jù)上面代碼我們逐步分析:
- 代碼初始化時(shí),創(chuàng)建全局上下文的變量對象剪侮。
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
- 在
foo
創(chuàng)建時(shí)汤善,foo
的[[scope]]
屬性是:
foo.[[Scope]] = [
globalContext.VO
];
- 在
foo
激活時(shí)(進(jìn)入上下文),foo
上下文的活動(dòng)對象票彪。
fooContext.AO = {
y: 20,
bar: <reference to function>
};
-
foo
上下文的作用域鏈為:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
- 內(nèi)部函數(shù)
bar
創(chuàng)建時(shí)红淡,其[[scope]]
為:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
- 在
bar
激活時(shí),bar
上下文的活動(dòng)對象為:
barContext.AO = {
z: 30
};
-
bar
上下文的作用域鏈為:
bar.Scope= [
barContext.AO,
fooContext.AO,
globalContext.VO
];
閉包的原理
了解了上面的相關(guān)概念之后降铸,我們通過一個(gè)閉包的例子來分析一下閉包的形成原理在旱。
function add(){
var sum =5;
var func = function () {
console.log(sum);
}
return func;
}
var addFunc = add();
addFunc(); //5
根據(jù)上面代碼我們逐步分析:
-
Js
執(zhí)行流進(jìn)入全局執(zhí)行上下文環(huán)境時(shí),全局執(zhí)行上下文可表示為:
globalContext = {
VO: {
add: <reference to function>,
addFunc: undefined
},
this: window,
scope chain: window
}
- 當(dāng)
add
函數(shù)被調(diào)用時(shí),add
函數(shù)執(zhí)行上下文可表示為:
addContext = {
AO: {
sum: undefined //代碼進(jìn)入執(zhí)行階段時(shí)此處被賦值為5
func: undefined //代碼進(jìn)入執(zhí)行階段時(shí)此處被賦值為function (){console.log(sum);}
},
this: window,
scope chain: addContext.AO + globalContext.VO
}
add
函數(shù)執(zhí)行完畢后推掸,Js
執(zhí)行流回到全局上下文環(huán)境中桶蝎,將add
函數(shù)的返回值賦值給addFunc
。由于
addFunc
仍保存著func
函數(shù)的引用谅畅,所以add
函數(shù)執(zhí)行上下文從執(zhí)行上下文堆棧頂端彈出后并未被銷毀而是保存在內(nèi)存中登渣。當(dāng)
addFunc()
執(zhí)行時(shí),func
函數(shù)被調(diào)用毡泻,此時(shí)func
函數(shù)執(zhí)行上下文可表示為:
funcContext = {
this: window,
scope chain: addContext.AO + globalContext.VO
}
當(dāng)要訪問變量sum
時(shí)胜茧,func
的活動(dòng)對象中未能找到,則會(huì)沿著作用域鏈查找仇味,由于Js
遵循詞法作用域呻顽,作用域在函數(shù)創(chuàng)建階段就被確定,在add
函數(shù)的活動(dòng)對象中找到sum = 5
;
閉包的用法實(shí)戰(zhàn)
閉包可以用在許多地方丹墨。它的最大用處有兩個(gè)廊遍,一個(gè)是可以讀取函數(shù)內(nèi)部的變量,另一個(gè)就是讓這些變量的值始終保持在內(nèi)存中贩挣。本文閉包的用法不是重點(diǎn)內(nèi)容喉前,如果想了解更多方法,可以自行查閱資料王财。下面列舉幾個(gè)應(yīng)用方法卵迂。
1、延遲回調(diào)
var a = 10;
setTimeout(function () {
alert(a); // 10, after one second
}, 1000);
2搪搏、回調(diào)函數(shù)
//...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
// 當(dāng)數(shù)據(jù)就緒的時(shí)候狭握,才會(huì)調(diào)用;
// 這里,不論是在哪個(gè)上下文中創(chuàng)建
// 此時(shí)變量“x”的值已經(jīng)存在了
alert(x); // 10
};
//...
3疯溺、創(chuàng)建封裝的作用域來隱藏輔助對象
var foo = {};
// 初始化
(function (object) {
var x = 10;
object.getX = function _getX() {
return x;
};
})(foo);
alert(foo.getX()); // 獲得閉包 "x" – 10
總結(jié)
本文介紹了關(guān)于閉包以及閉包相關(guān)的知識论颅,如果對你有用,歡迎點(diǎn)贊收藏4涯邸J逊琛!??