前言
閉包是JS
中重要的內(nèi)容,對大多數(shù)人來說都會覺的閉包本身很好理解,不就是一個函數(shù)嵌套一個函數(shù)嗎?但是再深入解釋時,好像不知道要說些啥。不用擔(dān)心,相信看完這篇你對閉包的理解就不僅僅只停留在概念層面上了。
基本概念
1、閉包是什么?
官方對閉包的解釋是:一個擁有許多變量和綁定了這些變量的環(huán)境的表達式(通常是一個函數(shù)),因而這些變量也是該表達式的一部分。
通俗的解釋是:閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。
更清晰的講:閉包就是一個函數(shù),這個函數(shù)能夠訪問其他函數(shù)的作用域中的變量。
JS為什么使用閉包?
因為JS
中變量的作用域分為全局變量和局部變量。在函數(shù)外部無法讀取函數(shù)內(nèi)的局部變量。需要閉包來解決。
閉包帶來的問題?
濫用閉包,會造成內(nèi)存泄漏;由于閉包會使得函數(shù)中的變量都被保存在內(nèi)存中,內(nèi)存消耗很大,所以不能濫用閉包,否則會造成網(wǎng)頁的性能問題,在IE
中可能導(dǎo)致內(nèi)存泄露。解決方法是,在退出函數(shù)之前,將不使用的局部變量指向null
。
閉包相關(guān)概念
在了解閉包之前,先來了解作用域、執(zhí)行上下文、變量對象、活動對象、作用域鏈,這些將有助于對閉包的理解。
作用域(Scope)
作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標(biāo)識符)。
1、作用域分為
- 全局作用域
- 函數(shù)作用域(ES6新增了塊級作用域)
PS:這些相信很多人都知道,就不詳細舉例了
2、作用域共有兩種主要的工作模型
- 詞法(靜態(tài))作用域:作用域是在編寫代碼的時候確定的
- 動態(tài)作用域:作用域是在代碼運行的時候確定的
我們知道Javascript
使用的是詞法(靜態(tài))作用域
。
3、詞法(靜態(tài))作用域
理解靜態(tài)作用域之前,首先要先了解Js在編譯階段做了些什么事情。
Js編譯階段分為三個階段,下面概括一下三個階段:
1. 分詞/詞法分析(Tokenizing/Lexing)
其實我們寫的代碼就是字符串,在編譯的第一個階段里,把這些字符串轉(zhuǎn)成詞法單元(toekn)。
2. 解析/語法分析(Parsing)
在有了詞法單元之后,JS
還需要繼續(xù)分解代碼中的語法以便為JS
引擎減輕負擔(dān),通過詞法單元生成了一個抽象語法樹(Abstract Syntax Tree),它的作用是為JS
引擎構(gòu)造出一份程序語法樹,我們簡稱為AST
。
3. 代碼生成(raw code)
這個階段主要做的就是拿AST
來生成一份JS
語言內(nèi)部認(rèn)可的代碼。
靜態(tài)作用域是發(fā)生在編譯階段的第一個步驟當(dāng)中,也就是分詞/詞法分析階段。它有兩種可能,分詞和詞法分析,分詞是無狀態(tài)的,而詞法分析是有狀態(tài)的。
那我們?nèi)绾闻袛嘤袩o狀態(tài)呢?以var a = 1
為例。
- 如果詞法單元生成器在判斷
a
是否為一個獨立的詞法單元時,調(diào)用的是有狀態(tài)的解析規(guī)則(生成器不清楚它是否依賴于其他詞法單元,所以要進一步解析)。 - 如果它不用生成器判斷,是一條不用被賦予語意的代碼(暫時可以理解為不涉及作用域的代碼,因為js內(nèi)部定義什么樣的規(guī)則我們并不清楚),那就被列入分詞中了。
總的來說,如果詞法單元生成器拿不準(zhǔn)當(dāng)前詞法單元是否為獨立的,就進入詞法分析,否則就進入分詞階段。
簡單的說,詞法作用域就是定義在詞法階段的作用域。詞法作用域就是你編寫代碼時,變量和塊級作用域?qū)懺谀睦餂Q定的。當(dāng)詞法解析器處理代碼時,會保持作用域不變(除動態(tài)作用域)。
執(zhí)行上下文(Execution Contexts)
1、Javascript
中代碼的執(zhí)行上下文分為以下三種:
- 全局級別的代碼 – 這個是默認(rèn)的代碼運行環(huán)境,一旦代碼被載入,引擎最先進入的就是這個環(huán)境。
- 函數(shù)級別的代碼 – 當(dāng)執(zhí)行一個函數(shù)時,運行函數(shù)體中的代碼。
-
Eval
的代碼 – 在Eval
函數(shù)內(nèi)運行的代碼。
2、一個執(zhí)行的上下文可以抽象的理解為一個對象。每一個執(zhí)行的上下文都有一系列的屬性:
- 變量對象(variable object)
- this指針(this value)
- 作用域鏈(scope chain)
代碼表示如下:
Execution Contexts = {
variable object:變量對象/活動對象;
this value: this指針;
scope chain:作用域鏈;
}
3、如何管理執(zhí)行上下文
Js
的運行采用棧
的方式對執(zhí)行上下文進行管理,棧底始終是全局上下文,棧頂始終是正在被調(diào)用執(zhí)行的函數(shù)的執(zhí)行上下文。
實例
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í)行時,創(chuàng)建全局上下文,并push
到call stack
。 -
fun1()
被調(diào)用時,創(chuàng)建fun1
上下文,push
到call stack
。 -
fun2()
被調(diào)用時,創(chuàng)建fun2
上下文,push
到call stack
。 -
fun2()
執(zhí)行完畢,fun2
上下文pop
出棧,等待被回收。 -
fun1()
執(zhí)行完畢,fun1
上下文pop
出棧,等待被回收。 - 全局執(zhí)行環(huán)境不會出棧。
4、執(zhí)行環(huán)境的生命周期
執(zhí)行上下文生命周期分為創(chuàng)建階段、執(zhí)行階段、執(zhí)行完畢。如下圖:
執(zhí)行上下文是代碼執(zhí)行的一種抽象,而代碼執(zhí)行除了整個Js開始執(zhí)行之外,代碼的執(zhí)行都是通過函數(shù)調(diào)用執(zhí)行的,所以執(zhí)行上下文生命周期的各個階段其實是可以分別對應(yīng)函數(shù)被調(diào)用時的初始化、執(zhí)行、執(zhí)行完畢階段的。下面會詳細的解釋每個階段的過程。
變量對象(variable object)
1、變量對象的定義:
如果變量與執(zhí)行上下文相關(guān),那變量自己應(yīng)該知道它的數(shù)據(jù)存儲在哪里,并且知道如何訪問。這種機制稱為變量對象(variable object)。
2、變量對象的作用:
可以說變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域(scope of data) 。它是與執(zhí)行上下文關(guān)聯(lián)的特殊對象,用于存儲被定義在執(zhí)行上下文中的變量(variables)、函數(shù)聲明(function declarations) 、arguments。
3、變量對象的創(chuàng)建過程:
實例
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ù)聲明,每檢查到一個函數(shù)聲明,就在變量對象中以函數(shù)名建立一個屬性,屬性指向函數(shù)所在的內(nèi)存地址。 - 檢查當(dāng)前執(zhí)行環(huán)境上的所有var變量聲明。每檢查到一個
var
聲明,如果VO
中已存在function
屬性名則跳過,如果沒有就在變量對象中以變量名新建一個屬性,屬性值為undefined
。
當(dāng)進入全局上下文時,全局上下文的變量對象可表示為:
VO = {
add: <reference to function>,
sum: undefined,
Math: <...>,
String: <...>
...
window: global //引用自身
}
活動對象(Activation Object)
當(dāng)函數(shù)被調(diào)用者激活時,這個特殊的活動對象(activation object) 就被創(chuàng)建了。它包含普通參數(shù)(formal parameters) 與特殊參數(shù)(arguments)對象(具有索引屬性的參數(shù)映射表)。活動對象在函數(shù)上下文中作為變量對象使用。
根據(jù)上圖,簡單解釋:在沒有執(zhí)行當(dāng)前環(huán)境之前,變量對象中的屬性都不能訪問!但是進入執(zhí)行階段之后,變量對象轉(zhuǎn)變?yōu)榱嘶顒訉ο?,里面的屬性都能被訪問了,然后開始進行執(zhí)行階段的操作。所以活動對象實際就是變量對象在真正執(zhí)行時的另一種形式。
根據(jù)上面變量對象的實例。當(dāng)add
函數(shù)被調(diào)用時,add
函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端,add
函數(shù)執(zhí)行上下文中活動對象可表示為
AO = {
num: 4,
sum: 5,
arguments:{0:4}
}
作用域鏈 (Scope Chain)
函數(shù)上下文的作用域鏈在函數(shù)調(diào)用時創(chuàng)建的,包含活動對象AO
和這個函數(shù)內(nèi)部的[[scope]]
屬性。
實例
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)地,它也不會添加到bar
的AO
中。乍一看,變量x
相對于函數(shù)bar
根本就不存在。
函數(shù)bar
如何訪問到變量x
?理論上函數(shù)應(yīng)該能訪問一個更高一層上下文的變量對象。實際上它正是這樣,這種機制是通過函數(shù)內(nèi)部的[[scope]]
屬性來實現(xiàn)的。
[[scope]]
是所有父級變量對象的層級鏈,處于當(dāng)前函數(shù)上下文之上,在函數(shù)創(chuàng)建時存于其中。
根據(jù)上面代碼我們逐步分析:
- 代碼初始化時,創(chuàng)建全局上下文的變量對象。
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
- 在
foo
創(chuàng)建時,foo
的[[scope]]
屬性是:
foo.[[Scope]] = [
globalContext.VO
];
- 在
foo
激活時(進入上下文),foo
上下文的活動對象。
fooContext.AO = {
y: 20,
bar: <reference to function>
};
-
foo
上下文的作用域鏈為:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
- 內(nèi)部函數(shù)
bar
創(chuàng)建時,其[[scope]]
為:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
- 在
bar
激活時,bar
上下文的活動對象為:
barContext.AO = {
z: 30
};
-
bar
上下文的作用域鏈為:
bar.Scope= [
barContext.AO,
fooContext.AO,
globalContext.VO
];
閉包的原理
了解了上面的相關(guān)概念之后,我們通過一個閉包的例子來分析一下閉包的形成原理。
function add(){
var sum =5;
var func = function () {
console.log(sum);
}
return func;
}
var addFunc = add();
addFunc(); //5
根據(jù)上面代碼我們逐步分析:
-
Js
執(zhí)行流進入全局執(zhí)行上下文環(huán)境時,全局執(zhí)行上下文可表示為:
globalContext = {
VO: {
add: <reference to function>,
addFunc: undefined
},
this: window,
scope chain: window
}
- 當(dāng)
add
函數(shù)被調(diào)用時,add
函數(shù)執(zhí)行上下文可表示為:
addContext = {
AO: {
sum: undefined //代碼進入執(zhí)行階段時此處被賦值為5
func: undefined //代碼進入執(zhí)行階段時此處被賦值為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í)行時,func
函數(shù)被調(diào)用,此時func
函數(shù)執(zhí)行上下文可表示為:
funcContext = {
this: window,
scope chain: addContext.AO + globalContext.VO
}
當(dāng)要訪問變量sum
時,func
的活動對象中未能找到,則會沿著作用域鏈查找,由于Js
遵循詞法作用域,作用域在函數(shù)創(chuàng)建階段就被確定,在add
函數(shù)的活動對象中找到sum = 5
;
閉包的用法實戰(zhàn)
閉包可以用在許多地方。它的最大用處有兩個,一個是可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量的值始終保持在內(nèi)存中。本文閉包的用法不是重點內(nèi)容,如果想了解更多方法,可以自行查閱資料。下面列舉幾個應(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ù)就緒的時候,才會調(diào)用;
// 這里,不論是在哪個上下文中創(chuàng)建
// 此時變量“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)的知識,如果對你有用,歡迎點贊收藏?。?!??