從執(zhí)行上下文(ES3,ES5)的角度來(lái)理解"閉包"

來(lái)源 | https://www.cnblogs.com/echoyya/p/14776868.html
介紹執(zhí)行上下文和執(zhí)行上下文棧概念
執(zhí)行上下文
執(zhí)行上下文棧
偽代碼模擬分析以下代碼中執(zhí)行上下文棧的行為
function a() {b()}function b() {c()}function c() {console.log('c');}a()
定義一個(gè)數(shù)組來(lái)模擬執(zhí)行上下文棧的行為: ECStack = [];
當(dāng) JavaScript 開(kāi)始要解釋執(zhí)行代碼時(shí),最先遇到肯定是全局代碼,所以初始化的時(shí)候首先就會(huì)向執(zhí)行上下文棧壓入一個(gè)全局執(zhí)行上下文,用 globalContext 表示它,并且只有當(dāng)整個(gè)應(yīng)用程序結(jié)束的時(shí)候,ECStack 才會(huì)被清空,所以程序結(jié)束之前, ECStack 最底部永遠(yuǎn)有個(gè) globalContext:
ECStack = [globalContext];
執(zhí)行一個(gè)函數(shù),都會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,并且壓入執(zhí)行上下文棧中的棧頂,當(dāng)函數(shù)執(zhí)行完畢后,就會(huì)將該函數(shù)的執(zhí)行上下文從棧頂彈出。
// 按照?qǐng)?zhí)行順序,分別創(chuàng)建對(duì)應(yīng)函數(shù)的執(zhí)行上下文,并且壓入執(zhí)行上下文棧的棧頂ECStack.push(functionAContext) // push aECStack.push(functionBContext) // push bECStack.push(functionCContext) // push c// 棧執(zhí)行,首先C函數(shù)執(zhí)行完畢,先進(jìn)后出,ECStack.pop() // 彈出cECStack.pop() // 彈出bECStack.pop() // 彈出a// javascript接著執(zhí)行下面的代碼,但是ECStack底層永遠(yuǎn)有個(gè)globalContext,直到整個(gè)應(yīng)用程序結(jié)束的時(shí)候,ECStack 才會(huì)被清空// ......// ......
代碼模擬實(shí)現(xiàn)棧的執(zhí)行過(guò)程
class Stack {constructor(){this.items = []}push(ele) {this.items.push(ele)}pop() {return this.items.pop()}}let stack = new Stack()stack.push(1)stack.push(2)stack.push(3)console.log(stack.pop()) // 3console.log(stack.pop()) // 2console.log(stack.pop()) // 1
通過(guò)ES3提出的老概念—理解執(zhí)行上下文
我在閱讀相關(guān)資料時(shí),遇到了一個(gè)問(wèn)題,就是關(guān)于執(zhí)行上下文說(shuō)法不一,不過(guò)大致可以分為兩種觀點(diǎn),一個(gè)是變量對(duì)象,活動(dòng)對(duì)象,詞法作用域,作用域鏈,另一個(gè)是詞法環(huán)境,變量環(huán)境,一番查閱可以確定的是,變量對(duì)象與活動(dòng)對(duì)象的概念是ES3提出的老概念,從ES5開(kāi)始就用詞法環(huán)境和變量環(huán)境替代了,因?yàn)楦媒忉尅?/span>
先大致講一下變量對(duì)象,活動(dòng)對(duì)象,詞法作用域,作用域鏈吧
1、變量對(duì)象和活動(dòng)對(duì)象
變量對(duì)象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲(chǔ)了在上下文中定義的變量和函數(shù)聲明。不同執(zhí)行上下文中的變量對(duì)象不同,分別看一下全局上下文中的變量對(duì)象和函數(shù)上下文中的變量對(duì)象。
1)、全局上下文中的變量對(duì)象
全局上下文中的變量對(duì)象就是全局對(duì)象。W3School 中有介紹:
全局對(duì)象是預(yù)定義的對(duì)象,作為 JavaScript 的全局函數(shù)和全局屬性的占位符。通過(guò)使用全局對(duì)象,可以訪問(wèn)所有其他所有預(yù)定義的對(duì)象、函數(shù)和屬性。
在頂層 JavaScript 代碼中,可以用關(guān)鍵字 this 引用全局對(duì)象。因?yàn)槿謱?duì)象是作用域鏈的頭,這意味著所有非限定性的變量和函數(shù)名都會(huì)作為該對(duì)象的屬性來(lái)查詢。
2)、函數(shù)上下文中的變量對(duì)象
在函數(shù)上下文中用活動(dòng)對(duì)象(activation object, AO)來(lái)表示變量對(duì)象(VO)。
活動(dòng)對(duì)象和變量對(duì)象其實(shí)是一個(gè)東西,只是變量對(duì)象是規(guī)范上的或者說(shuō)是引擎實(shí)現(xiàn)上的,不可在 JavaScript 環(huán)境中訪問(wèn),只有進(jìn)入一個(gè)執(zhí)行上下文中時(shí),這個(gè)執(zhí)行上下文的變量對(duì)象才會(huì)被激活,所以才叫活動(dòng)對(duì)象,而只有被激活的變量對(duì)象(也就是活動(dòng)對(duì)象)上的各種屬性才能被訪問(wèn)。
換句話說(shuō):未進(jìn)入執(zhí)行階段之前,變量對(duì)象(VO)中的屬性都不能訪問(wèn)!進(jìn)入執(zhí)行階段之后,變量對(duì)象(VO)轉(zhuǎn)變?yōu)榱嘶顒?dòng)對(duì)象(AO),里面的屬性可以被訪問(wèn),并開(kāi)始進(jìn)行執(zhí)行階段的操作。它們其實(shí)都是同一個(gè)對(duì)象,只是處于執(zhí)行上下文的不同生命周期
但是從嚴(yán)格角度來(lái)說(shuō),AO 實(shí)際上是包含了 VO 的。因?yàn)槌?VO 之外,AO 還包含函數(shù)的 parameters,以及 arguments 這個(gè)特殊對(duì)象。也就是說(shuō) AO 的確是在進(jìn)入到執(zhí)行階段的時(shí)候被激活,但是激活的除了 VO 之外,還包括函數(shù)執(zhí)行時(shí)傳入的參數(shù)和 arguments 這個(gè)特殊對(duì)象。
AO = VO + function parameters + arguments
活動(dòng)對(duì)象是在進(jìn)入函數(shù)上下文時(shí)刻被激活,通過(guò)函數(shù)的 arguments 屬性初始化。
執(zhí)行上下文的代碼會(huì)分成兩個(gè)階段進(jìn)行處理,預(yù)解析和執(zhí)行:
預(yù)解析的過(guò)程會(huì)激活A(yù)O對(duì)象,解析形參,變量提升及函數(shù)聲明等
在代碼執(zhí)行階段,會(huì)從上到下順序執(zhí)行代碼,根據(jù)代碼,修改變量對(duì)象的值。
2、詞法作用域
作用域是指代碼中定義變量的區(qū)域。其規(guī)定了如何查找變量,也就是確定當(dāng)前執(zhí)行代碼對(duì)變量的訪問(wèn)權(quán)限。
JavaScript 采用詞法作用域(lexical scoping),也就是靜態(tài)作用域,函數(shù)的作用域在函數(shù)定義的時(shí)候就決定了。
詞法作用域根據(jù)源代碼中聲明變量的位置來(lái)確定該變量在何處可用。嵌套函數(shù)可訪問(wèn)聲明于它們外部作用域的變量
// 詞法作用域var value = 1;function foo() {console.log(value);}function bar() {var value = 2;foo();}bar(); // 1
分析下執(zhí)行過(guò)程:執(zhí)行 foo ,先從 foo 內(nèi)部查找是否有局部變量 value,如果沒(méi)有,就根據(jù)書(shū)寫(xiě)的位置,查找上一層作用域,也就是 value=1,所以打印 1。
看個(gè)例子
var scope = "global scope";function checkscope(){var scope = "local scope";function f(){return scope;}return f();}checkscope();
var scope = "global scope";function checkscope(){var scope = "local scope";function f(){return scope;}return f;}checkscope()();
兩段代碼都會(huì)打?。簂ocal scope。原因也很簡(jiǎn)單,因?yàn)镴avaScript采用的是詞法作用域,函數(shù)的作用域基于函數(shù)創(chuàng)建的位置。
雖然兩段代碼執(zhí)行的結(jié)果一樣,但是兩段代碼究竟有什么不同呢?詞法作用域只是其中的一小部分,還有一個(gè)答案就是:執(zhí)行上下文棧的變化不一樣。
模擬第一段代碼運(yùn)行時(shí)棧中的變化:
ECStack.push(<checkscope> functionContext);ECStack.push(<f> functionContext);ECStack.pop();ECStack.pop();
模擬第二段代碼運(yùn)行時(shí)棧中的變化:
ECStack.push(<checkscope> functionContext);ECStack.pop();ECStack.push(<f> functionContext);ECStack.pop();
3、作用域鏈
每個(gè)函數(shù)都有自己的執(zhí)行上下文環(huán)境,當(dāng)代碼在這個(gè)環(huán)境中執(zhí)行時(shí),會(huì)創(chuàng)建變量對(duì)象的作用域鏈,作用域鏈類(lèi)似一個(gè)對(duì)象列表,它保證了變量對(duì)象的有序訪問(wèn)。
作用域鏈的最前端是當(dāng)前代碼執(zhí)行環(huán)境的變量對(duì)象,也稱“活躍對(duì)象AO”,當(dāng)查找變量的時(shí)候,會(huì)先從當(dāng)前上下文的變量對(duì)象中查找,如果找到就停止查找,如果沒(méi)有就會(huì)繼續(xù)向上級(jí)作用域(父級(jí)執(zhí)行上下文的變量對(duì)象)查找,直到找到全局上下文的變量對(duì)象(全局對(duì)象)
特別注意:作用域鏈的逐級(jí)查找,也會(huì)影響到程序的性能,變量作用域鏈越長(zhǎng)對(duì)性能影響越大,這也是為什么要盡量避免使用全局變量的一個(gè)主要原因。
那么這個(gè)作用域鏈?zhǔn)窃趺葱纬傻哪兀?/span>
這是因?yàn)楹瘮?shù)有一個(gè)內(nèi)部屬性 [[scope]]:當(dāng)函數(shù)創(chuàng)建時(shí),會(huì)保存所有父變量對(duì)象到其中,可以理解 [[scope]] 就是所有父變量對(duì)象的層級(jí)鏈,當(dāng)函數(shù)激活時(shí),進(jìn)入函數(shù)上下文,會(huì)將當(dāng)前激活的活動(dòng)對(duì)象添加到作用鏈的最前端。
此時(shí)就可以理解,查找變量時(shí)首先找自己,沒(méi)有再找父親。
下面以一個(gè)函數(shù)的創(chuàng)建和激活兩個(gè)時(shí)期來(lái)講解作用域鏈?zhǔn)侨绾蝿?chuàng)建和變化的。
function foo() {function bar() {...}}
函數(shù)創(chuàng)建時(shí),各自的[[scope]]為:
foo.[[scope]] = [globalContext.VO];bar.[[scope]] = [fooContext.AO,globalContext.VO];
當(dāng)函數(shù)激活時(shí),進(jìn)入函數(shù)上下文,就會(huì)將當(dāng)前激活的活動(dòng)對(duì)象添加到作用鏈的前端。
這時(shí)候當(dāng)前的執(zhí)行上下文的作用域鏈為 Scope = [AO].concat([[Scope]]);
以下面代碼為例,結(jié)合變量對(duì)象和執(zhí)行上下文棧,來(lái)總結(jié)一下函數(shù)執(zhí)行上下文中作用域鏈和變量對(duì)象的創(chuàng)建過(guò)程。
var scope = "global scope";function checkscope(){var scope2 = 'local scope';return scope2;}checkscope();
執(zhí)行過(guò)程如下(偽代碼):
1.checkscope 函數(shù)被創(chuàng)建,保存父變量對(duì)象到 內(nèi)部屬性[[scope]]= [globalContext.VO];2.執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,checkscope 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧ECStack = [checkscopeContext,globalContext];3.checkscope 函數(shù)并不立刻執(zhí)行,開(kāi)始做準(zhǔn)備工作,第一步:復(fù)制函數(shù)[[scope]]屬性創(chuàng)建作用域鏈checkscopeContext = {Scope: checkscope.[[scope]],}4.用 arguments 創(chuàng)建活動(dòng)對(duì)象,隨后初始化活動(dòng)對(duì)象,加入形參、函數(shù)聲明、變量聲明checkscopeContext = {AO: {arguments: {length: 0},scope2: undefined},Scope: checkscope.[[scope]],}5.將活動(dòng)對(duì)象壓入 checkscope 作用域鏈Scope的頂端checkscopeContext = {AO: {arguments: {length: 0},scope2: undefined},Scope: [AO, [[Scope]]]}6.準(zhǔn)備工作做完,開(kāi)始執(zhí)行函數(shù),隨著函數(shù)的執(zhí)行,修改 AO 的屬性值checkscopeContext = {AO: {arguments: {length: 0},scope2: 'local scope'},Scope: [AO, [[Scope]]]}7.查找到 scope2 的值,返回后函數(shù)執(zhí)行完畢,函數(shù)上下文從執(zhí)行上下文棧中彈出ECStack = [globalContext];
4、活學(xué)活用 — 案例分析
通過(guò)案例分析的形式,串聯(lián)上述所有知識(shí)點(diǎn),模擬執(zhí)行上下文創(chuàng)建執(zhí)行的過(guò)程
var scope = "global scope";function checkscope(){var scope = "local scope";function f(){return scope;}return f();}checkscope();// 1.執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文,全局上下文被壓入執(zhí)行上下文棧ECStack = [globalContext];// 2.全局上下文初始化globalContext = {VO: [global],Scope: [globalContext.VO],this: globalContext.VO}// 3.初始化的同時(shí),checkscope 函數(shù)被創(chuàng)建,保存作用域鏈到函數(shù)的內(nèi)部屬性[[scope]]checkscope.[[scope]] = [globalContext.VO];// 4.執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,并壓入執(zhí)行上下文棧ECStack = [checkscopeContext,globalContext];// 5.checkscope 函數(shù)執(zhí)行上下文初始化:/*** 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈,* 用 arguments 創(chuàng)建活動(dòng)對(duì)象,* 初始化活動(dòng)對(duì)象,即加入形參、函數(shù)聲明、變量聲明,* 將活動(dòng)對(duì)象壓入 checkscope 作用域鏈頂端。* 同時(shí) f 函數(shù)被創(chuàng)建,保存作用域鏈到 f 函數(shù)的內(nèi)部屬性[[scope]]*/checkscopeContext = {AO: {arguments: {length: 0},scope: undefined,f: reference to function f(){} // 引用函數(shù)},Scope: [AO, globalContext.VO],this: undefined}// 6.執(zhí)行 f 函數(shù),創(chuàng)建 f 函數(shù)執(zhí)行上下文,f 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧ECStack = [fContext,checkscopeContext,globalContext];// 7.f 函數(shù)執(zhí)行上下文初始化, 以下跟第 5 步相同:/**復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈用 arguments 創(chuàng)建活動(dòng)對(duì)象初始化活動(dòng)對(duì)象,即加入形參、函數(shù)聲明、變量聲明將活動(dòng)對(duì)象壓入 f 作用域鏈頂端*/fContext = {AO: {arguments: {length: 0}},Scope: [AO, checkscopeContext.AO, globalContext.VO],this: undefined}// 8.f 函數(shù)執(zhí)行,沿著作用域鏈查找 scope 值,返回 scope 值// 9.f 函數(shù)執(zhí)行完畢,f 函數(shù)上下文從執(zhí)行上下文棧中彈出ECStack = [checkscopeContext,globalContext];// 10.checkscope 函數(shù)執(zhí)行完畢,checkscope 執(zhí)行上下文從執(zhí)行上下文棧中彈出ECStack = [globalContext];
通過(guò)ES5提出的新概念—理解執(zhí)行上下文
執(zhí)行上下文創(chuàng)建分為創(chuàng)建階段與執(zhí)行階段兩個(gè)階段,較為難理解應(yīng)該是創(chuàng)建階段。
創(chuàng)建階段主要負(fù)責(zé)三件事:
確定this
創(chuàng)建詞法環(huán)境(LexicalEnvironment)
創(chuàng)建變量環(huán)境(VariableEnvironment)
1、創(chuàng)建階段
ExecutionContext = {ThisBinding = <this value>, // 確定thisLexicalEnvironment = {}, // 創(chuàng)建詞法環(huán)境VariableEnvironment = {}, // 創(chuàng)建變量環(huán)境};
1)、確定this
官方稱呼為:This Binding,在全局執(zhí)行上下文中,this總是指向全局對(duì)象,例如瀏覽器環(huán)境下this指向window對(duì)象。
而在函數(shù)執(zhí)行上下文中,this的值取決于函數(shù)的調(diào)用方式,如果被一個(gè)對(duì)象調(diào)用,那么this指向這個(gè)對(duì)象。否則this一般指向全局對(duì)象window或者undefined(嚴(yán)格模式)。
2)、詞法環(huán)境
詞法環(huán)境中包含標(biāo)識(shí)符和變量的映射關(guān)系,標(biāo)識(shí)符表示變量/函數(shù)的名稱,變量是對(duì)實(shí)際對(duì)象【包括函數(shù)類(lèi)型對(duì)象】或原始值的引用。
詞法環(huán)境由環(huán)境記錄與外部環(huán)境引入記錄兩個(gè)部分組成。
環(huán)境記錄:用于存儲(chǔ)當(dāng)前環(huán)境中的變量和函數(shù)聲明的實(shí)際位置;
外部環(huán)境引入記錄:用于保存自身環(huán)境可以訪問(wèn)的其它外部環(huán)境,有點(diǎn)作用域鏈的意思
那么全局執(zhí)行上下文與函數(shù)執(zhí)行上下文,也導(dǎo)致了詞法環(huán)境分為全局詞法環(huán)境和函數(shù)詞法環(huán)境兩種。
全局詞法環(huán)境:對(duì)外部環(huán)境的引入記錄為 null,因?yàn)樗旧砭褪亲钔鈱迎h(huán)境,除此之外它還記錄了當(dāng)前環(huán)境下的所有屬性、方法位置。
函數(shù)詞法環(huán)境:包含用戶在函數(shù)中定義的所有屬性方法外,還包含一個(gè)arguments對(duì)象(該對(duì)象包含了索引和傳遞給函數(shù)的參數(shù)之間的映射以及傳遞給函數(shù)的參數(shù)的長(zhǎng)度)。函數(shù)詞法環(huán)境的外部環(huán)境引入可以是全局環(huán)境,也可以是其它函數(shù)環(huán)境,這個(gè)根據(jù)實(shí)際代碼而定。
環(huán)境記錄在全局和函數(shù)中也不同,全局中的環(huán)境記錄叫對(duì)象環(huán)境記錄,函數(shù)中環(huán)境記錄叫聲明性環(huán)境記錄,下方有展示:
對(duì)象環(huán)境記錄: 用于定義在全局執(zhí)行上下文中出現(xiàn)的變量和函數(shù)的關(guān)聯(lián)。全局環(huán)境包含對(duì)象環(huán)境記錄。
聲明性環(huán)境記錄 存儲(chǔ)變量、函數(shù)和參數(shù)。一個(gè)函數(shù)環(huán)境包含聲明性環(huán)境記錄。
GlobalExectionContext = { // 全局環(huán)境LexicalEnvironment: { // 全局詞法環(huán)境EnvironmentRecord: { // 類(lèi)型為對(duì)象環(huán)境記錄Type: "Object",// 標(biāo)識(shí)符綁定在這里},outer: < null >}};FunctionExectionContext = { // 函數(shù)環(huán)境LexicalEnvironment: { // 函數(shù)詞法環(huán)境EnvironmentRecord: { // 類(lèi)型為聲明性環(huán)境記錄Type: "Declarative",// 標(biāo)識(shí)符綁定在這里},outer: < Global or outerfunction environment reference >}};
3)、變量環(huán)境
它也是一個(gè)詞法環(huán)境,它具備詞法環(huán)境所有屬性,同樣有環(huán)境記錄與外部環(huán)境引入。
在ES6中唯一的區(qū)別在于詞法環(huán)境用于存儲(chǔ)函數(shù)聲明與let const聲明的變量,而變量環(huán)境僅僅存儲(chǔ)var聲明的變量。
let a = 20;const b = 30;var c;function multiply(e, f) {var g = 20;return e * f * g;}c = multiply(20, 30);
//全局執(zhí)行上下文GlobalExectionContext = {ThisBinding: Global Object, // this綁定為全局對(duì)象LexicalEnvironment: { // 詞法環(huán)境EnvironmentRecord: {Type: "Object", // 對(duì)象環(huán)境記錄// let const創(chuàng)建的變量a b在這a: uninitialized ,b: uninitialized ,multiply: < func >}outer: null // 全局環(huán)境外部環(huán)境引入為null},VariableEnvironment: { // 變量環(huán)境EnvironmentRecord: {Type: "Object", // 對(duì)象環(huán)境記錄// var創(chuàng)建的c在這c: undefined,}outer: null // 全局環(huán)境外部環(huán)境引入為null}}// 函數(shù)執(zhí)行上下文FunctionExectionContext = {ThisBinding: Global Object, //由于函數(shù)是默認(rèn)調(diào)用 this綁定同樣是全局對(duì)象LexicalEnvironment: { // 詞法環(huán)境EnvironmentRecord: {Type: "Declarative", // 聲明性環(huán)境記錄// arguments對(duì)象在這Arguments: {0: 20, 1: 30, length: 2},},outer: GlobalEnvironment // 外部環(huán)境引入記錄為Global},VariableEnvironment: { // 變量環(huán)境EnvironmentRecord: {Type: "Declarative", // 聲明性環(huán)境記錄// var創(chuàng)建的g在這g: undefined},outer: GlobalEnvironment // 外部環(huán)境引入記錄為Global}}
這會(huì)引發(fā)我們另外一個(gè)思考,那就是變量提升的原因:
我們會(huì)發(fā)現(xiàn)在創(chuàng)建階段,代碼會(huì)被掃描并解析變量和函數(shù)聲明,其中l(wèi)et 和 const 定義的變量沒(méi)有任何與之關(guān)聯(lián)的值,會(huì)保持未初始化的狀態(tài)。
但 var 定義的變量設(shè)置為 undefined。
所以這就是為什么可以在聲明之前,訪問(wèn)到 var 聲明的變量(盡管是 undefined),但如果在聲明之前訪問(wèn) let 和 const 聲明的變量就會(huì)報(bào)錯(cuò)的原因,也就是常說(shuō)的暫時(shí)性死區(qū),
在執(zhí)行上下文創(chuàng)建階段,函數(shù)聲明與var聲明的變量在創(chuàng)建階段已經(jīng)被賦予了一個(gè)值,var聲明被設(shè)置為了undefined,函數(shù)被設(shè)置為了自身函數(shù),而let const被設(shè)置為未初始化。這是因?yàn)閳?zhí)行上下文創(chuàng)建階段JS引擎對(duì)兩者初始化賦值不同。
執(zhí)行階段
上下文除了創(chuàng)建階段外,還有執(zhí)行階段,代碼執(zhí)行時(shí)根據(jù)之前的環(huán)境記錄對(duì)應(yīng)賦值,比如早期var在創(chuàng)建階段為undefined,如果有值就對(duì)應(yīng)賦值,像let const值為未初始化,如果有值就賦值,無(wú)值則賦予undefined。
執(zhí)行上下文總結(jié)
全局執(zhí)行上下文一般由瀏覽器創(chuàng)建,代碼執(zhí)行時(shí)就會(huì)創(chuàng)建;函數(shù)執(zhí)行上下文只有函數(shù)被調(diào)用時(shí)才會(huì)創(chuàng)建,同一個(gè)函數(shù)被多次調(diào)用,都會(huì)創(chuàng)建一個(gè)新的上下文。
調(diào)用棧用于存放所有執(zhí)行上下文,滿足FILO特性。
執(zhí)行上下文創(chuàng)建階段分為綁定this,創(chuàng)建詞法環(huán)境,變量環(huán)境三步,兩者區(qū)別在于詞法環(huán)境存放函數(shù)聲明與const let聲明的變量,而變量環(huán)境只存儲(chǔ)var聲明的變量。
詞法環(huán)境主要由環(huán)境記錄與外部環(huán)境引入記錄兩個(gè)部分組成,全局上下文與函數(shù)上下文的外部環(huán)境引入記錄不一樣,全局為null,函數(shù)為全局環(huán)境或者其它函數(shù)環(huán)境。環(huán)境記錄也不一樣,全局叫對(duì)象環(huán)境記錄,函數(shù)叫聲明性環(huán)境記錄。
ES3之前的變量對(duì)象與活動(dòng)對(duì)象的概念在ES5之后由詞法環(huán)境,變量環(huán)境來(lái)解釋?zhuān)瑑烧吒拍畈粵_突,后者理解更為通俗易懂。
閉包
上文說(shuō)了這么多,其實(shí)我本意只是想聊一聊閉包的,終于回歸正題。
閉包是什么?
MDN 對(duì)閉包的定義簡(jiǎn)單理解就是:
閉包是由函數(shù)以及聲明該函數(shù)的詞法環(huán)境組合而成的。該環(huán)境包含了這個(gè)閉包創(chuàng)建時(shí)作用域內(nèi)的任何局部變量(閉包維持了一個(gè)對(duì)它的詞法環(huán)境的引用:在一個(gè)函數(shù)內(nèi)部定義的函數(shù),會(huì)將外部函數(shù)的活躍對(duì)象添加到自己的作用域鏈中)。
所以可以在一個(gè)內(nèi)層函數(shù)中訪問(wèn)到其外層函數(shù)的作用域。在 JavaScript 中,每當(dāng)創(chuàng)建一個(gè)函數(shù),閉包就會(huì)在函數(shù)創(chuàng)建的同時(shí)被創(chuàng)建出來(lái)。
人們常說(shuō)的閉包無(wú)非就是:函數(shù)內(nèi)部返回一個(gè)函數(shù),一是可以讀取并操作函數(shù)內(nèi)部的變量,二是可以讓這些變量的值始終保存在內(nèi)存中。
而在《JavaScript權(quán)威指南》中講到:從理論的角度講,所有的JavaScript函數(shù)都是閉包。:
從理論角度:所有的函數(shù)。因?yàn)樗鼈兌荚趧?chuàng)建時(shí)保存了上層上下文的數(shù)據(jù)。哪怕是簡(jiǎn)單的全局變量也是如此,因?yàn)楹瘮?shù)中訪問(wèn)全局變量就相當(dāng)于是在訪問(wèn)自由變量,這個(gè)時(shí)候使用最外層的作用域。
從實(shí)踐角度:閉包無(wú)非滿足以下兩點(diǎn):
閉包首先得是一個(gè)函數(shù)。
閉包能訪問(wèn)外部函數(shù)作用域中的自由變量,即使外部函數(shù)上下文已銷(xiāo)毀。(也可以理解為是自帶了執(zhí)行環(huán)境的函數(shù))
閉包的形成與實(shí)現(xiàn)
上文中介紹過(guò)JavaScript是采用詞法作用域的,講的是函數(shù)的執(zhí)行依賴于函數(shù)定義的時(shí)候所產(chǎn)生的變量作用域。
為了去實(shí)現(xiàn)這種詞法作用域,JavaScript函數(shù)對(duì)象的內(nèi)部狀態(tài)不僅包含函數(shù)邏輯的代碼,還包含當(dāng)前作用域鏈的引用。
函數(shù)對(duì)象可以通過(guò)這個(gè)作用域鏈相互關(guān)聯(lián)起來(lái),函數(shù)體內(nèi)部的變量都可以保存在函數(shù)的作用域內(nèi)
let scope = "global scope";function checkscope() {let scope = "local scope"; // 自由變量function f() { // 閉包console.log(scope);};return f;};let foo = checkscope();foo();
// 1. 偽代碼分別表示執(zhí)行棧中上下文的變化,以及上下文創(chuàng)建的過(guò)程,首先執(zhí)行棧中永遠(yuǎn)都會(huì)存在一個(gè)全局執(zhí)行上下文。ECStack = [GlobalExecutionContext];// 2. 此時(shí)全局上下文中存在兩個(gè)變量scope、foo與一個(gè)函數(shù)checkscope,上下文用偽代碼表示具體是這樣:GlobalExecutionContext = { // 全局執(zhí)行上下文ThisBinding: Global Object ,LexicalEnvironment: { // 詞法環(huán)境EnvironmentRecord: {Type: "Object", // 對(duì)象環(huán)境記錄scope: uninitialized ,foo: uninitialized ,checkscope: func}outer: null // 全局環(huán)境外部環(huán)境引入為null}}// 3. 全局上下文創(chuàng)建階段結(jié)束,進(jìn)入執(zhí)行階段,全局執(zhí)行上下文的標(biāo)識(shí)符中像scope、foo之類(lèi)的變量被賦值,然后開(kāi)始執(zhí)行checkscope函數(shù),于是一個(gè)新的函數(shù)執(zhí)行上下文被創(chuàng)建,并壓入執(zhí)行棧中:ECStack = [checkscopeExecutionContext,GlobalExecutionContext];// 4. checkscope函數(shù)執(zhí)行上下文進(jìn)入創(chuàng)建階段:checkscopeExecutionContext = { // 函數(shù)執(zhí)行上下文ThisBinding: Global Object,LexicalEnvironment: { // 詞法環(huán)境EnvironmentRecord: {Type: "Declarative", // 聲明性環(huán)境記錄Arguments: {},scope: uninitialized ,f: func},outer: GlobalLexicalEnvironment // 外部環(huán)境引入記錄為<Global>}}// 5. checkscope() 等同于window.checkscope() ,所以checkExectionContext 中this指向全局,而且外部環(huán)境引用outer也指向了全局(作用域鏈),其次在標(biāo)識(shí)符中記錄了arguments對(duì)象以及變量scope與函數(shù)f// 6. 函數(shù) checkscope 執(zhí)行到返回函數(shù) f 時(shí),函數(shù)執(zhí)行完畢,checkscope 的執(zhí)行上下文被彈出執(zhí)行棧,所以此時(shí)執(zhí)行棧中又只剩下全局執(zhí)行上下文:ECStack = [GlobalExecutionContext];// 7. 代碼foo()執(zhí)行,創(chuàng)建foo的執(zhí)行上下文,ECStack = [fooExecutionContext, GlobalExecutionContext];// 8. foo的執(zhí)行上下文是這樣:fooExecutionContext = {ThisBinding: Global Object ,LexicalEnvironment: { // 詞法環(huán)境EnvironmentRecord: {Type: "Declarative", // 聲明性環(huán)境記錄Arguments: {},},outer: checkscopeEnvironment // 外部環(huán)境引入記錄為<checkscope>}}// 9. foo()等同于window.foo(),所以this指向全局window,但outer外部環(huán)境引入有點(diǎn)不同,指向了外層函數(shù) checkscope(原因是JS采用詞法作用域,也就是靜態(tài)作用域,函數(shù)的作用域在定義時(shí)就確定了,而不是執(zhí)行時(shí)確定)/*** a. 但是可以發(fā)現(xiàn)的是,現(xiàn)在執(zhí)行棧中只有 fooExecutionContext 和 GlobalExecutionContext, checkscopeExecutionContext 在執(zhí)行完后就被釋放了,怎么還能訪問(wèn)到 其中的變量?* b. 正常來(lái)說(shuō)確實(shí)是不可以,但是因?yàn)殚]包 foo 外部環(huán)境 outer 的引用,從而讓 checkscope作用域中的變量依舊存活在內(nèi)存中,無(wú)法被釋放,所以有時(shí)有必要手動(dòng)釋放自由變量。* c. 總結(jié)一句,閉包是指能使用其它作用域自由變量的函數(shù),即使作用域已銷(xiāo)毀。*/
閉包有什么用?
說(shuō)閉包聊閉包,結(jié)果閉包有啥用都不知道,甚至遇到了一個(gè)閉包第一時(shí)間都沒(méi)反應(yīng)過(guò)來(lái)這是閉包,說(shuō)說(shuō)閉包有啥用:
1)、模擬私有屬性、方法
所謂私有屬性方法其實(shí)就是這些屬性方法只能被同一個(gè)類(lèi)中的其它方法所調(diào)用,但是JavaScript中并未提供專(zhuān)門(mén)用于創(chuàng)建私有屬性的方法,但可以通過(guò)閉包模擬它:
私有方法不僅有利于限制對(duì)代碼的訪問(wèn):還提供了管理全局命名空間的強(qiáng)大能力,避免非核心的方法弄亂了代碼的公共接口部分。
例一:通過(guò)自執(zhí)行函數(shù)返回了一個(gè)對(duì)象,只創(chuàng)建了一個(gè)詞法環(huán)境,為三個(gè)閉包函數(shù)所共享:fn.increment,fn.decrement 和 fn.value,除了這三個(gè)方法能訪問(wèn)到變量privateCounter和 changeBy函數(shù)外,無(wú)法再通過(guò)其它手段操作它們。
let fn = (function () {var privateCounter = 0;function changeBy(val) {privateCounter += val;};return {increment: function () {changeBy(1);},decrement: function () {changeBy(-1);},value: function () {console.log(privateCounter);}};})();fn.value(); // 0fn.increment();fn.increment();fn.value(); // 2fn.decrement();fn.value(); // 1
例二:構(gòu)造函數(shù)中也有閉包:
function Echo(name) {var age = 26; // 私有屬性this.name = name; // 構(gòu)造器屬性this.hello = function () {console.log(`我的名字是${this.name},我今年${age}了`);};};var person = new Echo('yya');person.hello(); //我的名字是yya,我今年26了
若某個(gè)屬性方法在所有實(shí)例中都需要使用,一般都會(huì)推薦加在構(gòu)造函數(shù)的原型上,還有種做法就是利用私有屬性。
比如這個(gè)例子中所有實(shí)例都可以正常使用變量 age,將age稱為私有屬性的同時(shí),也會(huì)將this.hello稱為特權(quán)方法,因?yàn)橹挥型ㄟ^(guò)這個(gè)方法才能訪問(wèn)被保護(hù)的私有屬性age。
2)、工廠函數(shù)
使用函數(shù)工廠創(chuàng)建了兩個(gè)新函數(shù) — 一個(gè)將其參數(shù)和 5 求和,另一個(gè)和 10 求和。 add5 和 add10 都是閉包。
它們共享相同的函數(shù)定義,但是保存了不同的詞法環(huán)境。在 add5 的環(huán)境中,x 為 5。在 add10 中,x 則為 10。
利用了閉包自帶執(zhí)行環(huán)境的特性(即使外層作用域已銷(xiāo)毀),僅僅使用一個(gè)形參完成了兩個(gè)形參求和。
當(dāng)然例子函數(shù)還有個(gè)更專(zhuān)業(yè)的名詞,叫函數(shù)柯里化。
function makeAdder(x) {return function (y) {console.log(x + y);};};var add5 = makeAdder(5);var add10 = makeAdder(10);add5(2); // 7add10(2); // 12
閉包對(duì)性能和內(nèi)存的影響
閉包會(huì)額外附帶函數(shù)的作用域(內(nèi)部匿名函數(shù)攜帶外部函數(shù)的作用域),會(huì)比其它函數(shù)多占用些內(nèi)存空間,過(guò)度的使用可能會(huì)導(dǎo)致內(nèi)存占用的增加。
閉包中包含與函數(shù)執(zhí)行上下文相同的作用域鏈引用,因此會(huì)產(chǎn)生一定的負(fù)面作用,當(dāng)函數(shù)中活躍對(duì)象和執(zhí)行上下文銷(xiāo)毀時(shí),由于閉包仍存在對(duì)活躍對(duì)象的引用,導(dǎo)致活躍對(duì)象無(wú)法銷(xiāo)毀,可能會(huì)導(dǎo)致內(nèi)存泄漏。
閉包中如果存在對(duì)外部變量的訪問(wèn),會(huì)增加標(biāo)識(shí)符的查找路徑,在一定的情況下,也會(huì)造成性能方面的損失。解決此類(lèi)問(wèn)題的辦法:盡量將外部變量存入到局部變量中,減少作用域鏈的查找長(zhǎng)度。
綜上所述:如果不是某些特定任務(wù)需要使用閉包,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的,因?yàn)樵谔幚硭俣群蛢?nèi)存消耗方面對(duì)腳本性能具有負(fù)面影響。
了解了JS引擎的工作機(jī)制之后,我們不能只停留在理解概念的層面,而要將其作為基礎(chǔ)工具,用以優(yōu)化和改善我們?cè)趯?shí)際工作中的代碼,提高執(zhí)行效率,產(chǎn)生實(shí)際價(jià)值才是我們真正的目的。
就拿變量查找機(jī)制來(lái)說(shuō),如果代碼嵌套很深,每引用一次全局變量,JS引擎就要查找整個(gè)作用域鏈,比如處于作用域鏈的最底端window和document對(duì)象就存在這個(gè)問(wèn)題,因此我們圍繞這個(gè)問(wèn)題可以做很多性能優(yōu)化的工作,當(dāng)然還有其他方面的優(yōu)化,此處不再贅述,如果有幫到你,點(diǎn)個(gè)贊再走吧~~~
學(xué)習(xí)更多技能
請(qǐng)點(diǎn)擊下方公眾號(hào)
![]()

