JavaScript深入理解作用域鏈與閉包詳情
深入作用域鏈與閉包
為什么要把作用域鏈和閉包放在一起講呢,它們有什么關(guān)聯(lián)嗎?
試想,我們?nèi)绻谝粋€內(nèi)部的函數(shù)使用了外部的變量,是通過[[outerEnv]]
串起來的詞法環(huán)境(各類環(huán)境記錄),即最終在瀏覽器上的實(shí)現(xiàn),作用域鏈[[Scope]]
。
而閉包的觸發(fā),是需要在一個獨(dú)立的空間中管理從外部獲得的變量。而這個外部變量的獲取與綁定,則是需要通過作用域鏈。
所以理解了作用域鏈的形成原理,才能更好的深入理解閉包。
作用域鏈
上節(jié)的例子中對于函數(shù)中變量記錄的闡釋并不完備,只是簡單的將VariableEnvironemnt.[[outerEnv]]
指向了外部。仔細(xì)思考的同學(xué)可能會發(fā)現(xiàn),JavaScript里面萬物皆對象,函數(shù)這個對象滿天飛,如果每次都要解析全局詞法來獲取某個函數(shù)的外部環(huán)境,是不是很浪費(fèi)性能呢?
[[Environment]]
所以其實(shí)在函數(shù)被聲明的時候,就被加上了一個內(nèi)部屬性[[Environment]]
,根據(jù)規(guī)范定義,它也是一個環(huán)境記錄,其[[outerEnv]]
指向聲明函數(shù)的詞法環(huán)境。
10.2 ECMAScript Function Objects) Internal Slots of ECMAScript Function Objects
Internal Slot | Type | Description |
---|---|---|
[[Environment]] | an Environment Record | The Environment Record that the function was closed over. Used as the outer environment when evaluating the code of the function. |
完善環(huán)境記錄
同時,在函數(shù)執(zhí)行的時候,創(chuàng)建的詞法環(huán)境和變量環(huán)境都是存儲在 [[Environment]]
中的
?function foo() { ? ? ?var a = 1; ? ? ?let b = 2; ?} ?foo();
在函數(shù)執(zhí)行前創(chuàng)建上下文時,較為完備的解釋應(yīng)該如下:
?ExecutionContext: { ? ? [[Environment]](0x00): { ? ? ? ? ?LexicalEnvironment(0x01): { ? ? ? ? ? ? ?b -> nothing ? ? ? ? ? ? [[outerEnv]]: 0x02 ? ? ? ? } ? ? ? ? ?VariableEnvironment(0x02): { ? ? ? ? ? ? ?a -> undefined ? ? ? ? ? ? [[outerEnv]]: 0x00 ? ? ? ? } ? ? ? ? ?... ? ? ? ? [[outerEnv]]: global ? ? } ?}
閉包
函數(shù)實(shí)例
為了更好的解釋閉包。先了解一下函數(shù)實(shí)例化的概念:聲明函數(shù)的時候可以使用new Function
,實(shí)例出來一個函數(shù)對象
- 函數(shù)聲明:可以叫做函數(shù)實(shí)例化,創(chuàng)建了原型
Function
的一個實(shí)例 - 函數(shù)表達(dá)式:則為創(chuàng)建函數(shù)的實(shí)例
舉個例子說明同一個函數(shù)代碼塊能有多個函數(shù)實(shí)例:
?function foo() { ? return function myFun() {} ?} ?const fun1 = foo() ?const fun2 = foo() ?console.log(fun1 === fun2) // false
在這里,myFun
就是一個函數(shù)表達(dá)式,而fun1/fun2
就是兩個不同的實(shí)例
什么是閉包
基于這個概念,關(guān)于作用域鏈與閉包的關(guān)系可以這么理解:
每生成一個函數(shù)實(shí)例,實(shí)例內(nèi)部都會有一條由環(huán)境記錄(包括函數(shù)自身的)串成的作用域鏈。而閉包可以理解為是與函數(shù)實(shí)例的作用域鏈綁定的一個映像。
在具體實(shí)踐中(如V8引擎),函數(shù)在預(yù)編譯的時候會解析函數(shù)內(nèi)部的詞法,無論深度、子函數(shù)是否被調(diào)用,只要內(nèi)部有用到外部的變量,就會把它們存到同一個閉包上,由于這些變量是通過作用域鏈獲取且綁定的,所以可以說閉包只是一個作用域鏈的丐版復(fù)制品。
同時,這個閉包可以理解為父函數(shù)的一個屬性,且同一個實(shí)例中的所有子函數(shù)使用同一個閉包,后文會對這一點(diǎn)進(jìn)行驗(yàn)證。
變量綁定
為什么要提到綁定?
- 當(dāng)外部變量發(fā)生變化時,閉包中的對應(yīng)的變量也會發(fā)生變化。
- 在閉包中的使外部變量發(fā)生變化,其綁定的環(huán)境記錄中的變量也會變化。
?let a = 1; ?let b = 2; ?function foo() { ? ?return function () { ? ? ?a += 1; ? ? ?b += 10; ? ? ?console.log(a, b); ? }; ?} ?const bar = foo(); ?bar(); // 2 12 ?bar(); // 3 22 ?a += 10; ?bar(); // 14 32 ?a = 0; ?bar(); // 1 42 ?cnsole.log(a) // 1 (在閉包中+1,全局環(huán)境中的 a 也對應(yīng)+1)
這個綁定也可以解釋一個經(jīng)典的面試題(相關(guān)前置知識可以參考上一節(jié)《環(huán)境變量》)
?function foo() { ? ?for (var i = 0; i < 6; i++) { ? ? ?setTimeout(() => { ? ? ? ?console.log(i); ? ? }, i * 100); ? } ?} ?foo(); // 6 6 6 6 6
因?yàn)?nbsp;i
是使用 var
聲明的,所以會“逸出”保存到 foo
的變量環(huán)境中。因此setTimeout
中的匿名函數(shù)閉包中的 i
是與 foo
環(huán)境所綁定。當(dāng)執(zhí)行 i++
,即 foo
的 i++
,閉包中的 i
隨之變化。因?yàn)樗虚]包綁定了同一個環(huán)境記錄,所以是顯示同一個值,退出循環(huán)后仍然執(zhí)行了一次 i++
,因此輸出為 6 而不是 5。
對應(yīng)的。我們來看看用 let
聲明的 i
的表現(xiàn)。
?function foo() { ? ?for (let i = 0; i < 6; i++) { ? ? ?setTimeout(() => { ? ? ? ?console.log(i); ? ? }, i * 100); ? } ?}? ?foo(); // 1 2 3 4 5
這里的 i
是由 let
聲明,所以它會被保存到最近的詞法環(huán)境中,即塊的詞法環(huán)境。每次循環(huán)都會形成一個新的塊級作用域,因此 i
保存的環(huán)境都不一樣,即每個setTimeout
匿名函數(shù)閉包中的 i
綁定了不同的環(huán)境記錄。因此可以單獨(dú)管理。
同一個閉包
上文提到,在同一個函數(shù)實(shí)例中,所有子函數(shù)公用一個閉包。我們用具體代碼來驗(yàn)證一下
?function foo() { ? ?let a = 1; ? ?const b = 2; ? ?let c = 3; ? ?let d = 4; ? ?function bar() { // 驗(yàn)證深度以及沒有被調(diào)用的情況 ? ? ?console.log(a); ? ? ?function barSon() { ? ? ? ?console.log(b); ? ? } ? } ? ?return function () { ? ? ?console.log(d); ? ? ?return { ? ? ? ?addNum() { ? ? ? ? ?d = "new" + d; ? ? ? }, ? ? }; ? }; ?} ?const fun1 = foo(); ?fun1().addNum(); // 驗(yàn)證不同實(shí)例的閉包空間獨(dú)立 ?fun1(); ?const fun2 = foo(); ?fun2();
這里我們新建了兩個實(shí)例,按照上文的理論,二者的閉包應(yīng)該是獨(dú)立的,且所有子函數(shù)無論深度以及子函數(shù)是否被調(diào)用都會共用一個閉包。
上圖的包含變量 a,b
的子函數(shù)并沒有調(diào)用,但是在閉包中仍然存在。
返回的匿名函數(shù)和 bar
有使用到的變量在同一個閉包 foo.Closure
中
這里我們調(diào)用fun1.addNum
修改了 d
的值,但是實(shí)例 fun2
的閉包中的 d
仍然是 4 ??梢钥闯鰞蓚€閉包是獨(dú)立的。
總結(jié)
與靜態(tài)的函數(shù)實(shí)例相對應(yīng),閉包是一個運(yùn)行期的概念,其在函數(shù)執(zhí)行的過程中處于激活的、可訪問的狀態(tài)。并在函數(shù)實(shí)例調(diào)用結(jié)束后保持?jǐn)?shù)據(jù)信息的最終狀態(tài),直到閉包被銷毀。(具體體現(xiàn)形式為上個例子中不斷調(diào)用同一個函數(shù)會輸出不同而值,而這些值是來自于上次調(diào)用的最終數(shù)據(jù)狀態(tài))
實(shí)際上,函數(shù)執(zhí)行時,如果當(dāng)前函數(shù)有使用到外部變量,會新建一個環(huán)境記錄作為閉包(規(guī)范定義),它存在與 [[Environment]].[[outerEnv]]
的可變綁定。(在瀏覽器中,只要函數(shù)內(nèi)部對外部變量進(jìn)行引用,函數(shù)的內(nèi)部屬性[[Scope]]
中會有名為 Closure
的閉包)
下面是上一個例子執(zhí)行完畢之后兩個函數(shù)實(shí)例的[[Scope]]
在最后舉個例子形象說明一下閉包在作用域鏈中處于什么位置
?function foo() { ? ?let a = 1; ? ?const b = 2; ? ?let c = 3; ? ?let d = 4; ? ?function bar() { // 驗(yàn)證深度以及沒有被調(diào)用的情況 ? ? ?console.log(a); ? ? ?function barSon() { ? ? ? ?console.log(b); ? ? } ? } ? ?return function myFun() { ? ? ?console.log(d); ? }; ?}? ?const fun1 = foo();
?// 詞法編譯時,假設(shè)每個空間都有一個的堆內(nèi)存 ?ExecutionContext(foo): { ? ? [[outerEnv]]: global ? ... ? ? [[Environment]](0x00): { ? LexicalEnviroemnt(0x01): { ? ? ? ? ? ? [[outerEnv]]: 0x00 ? ... ? ? ? ? ? ? ?a —> nothing, b -> nothing, c -> nothing, d -> nothing ? ? ? ? ? ? ?bar: { ? ? ? ? ? ? ? ? ?LexicalEnviroemnt(0x02): {...} ? ? ? ? ? ? ? ? [[Environment]] : { ? ? ? ? ? ? ? ? ? ? [[outerEnv]] : 0x10 // 指向閉包 ? ? ? ? ? ? ? ? ? ? ?... ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ?barSon: { ? ? ? ? ? ? ? ? ? ? [[Environment]] : { ? ? ? ? ? ? ? ? ? ? ? ? ?// 指向上一層函數(shù)的詞法環(huán)境,如果有閉包則會指向上一層函數(shù)的閉包 ? ? ? ? ? ? ? ? ? ? ? ? [[outerEnv]] : 0x02 ? ? ? ? ? ? ? ? ? ? ? ? ?... ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? myFun: { ? ? ? ? ? ? ? ? [[Environment]] : { ? ? ? ? ? ? ? ? ? ? [[outerEnv]] : 0x10 // 指向閉包 ? ? ? ? ? ? ? ? ? ? ?... ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } ? // 即所謂的閉包,這里面變量的來源于外部的環(huán)境記錄(某種映射) ? EnvironmentRecord(0x10): { ? a -> nothing, b -> nothing, d -> nothing ? ? ? ? ? ? [[outerEnv]]: 0x01 // 指向外部詞法環(huán)境 ? ? ? ? } ? ? } ? ? ?... ?}
到此這篇關(guān)于JavaScript深入理解作用域鏈與閉包詳情的文章就介紹到這了,更多相關(guān)JS作用域鏈與閉包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
微信小程序?qū)崿F(xiàn)動態(tài)設(shè)置頁面標(biāo)題的方法【附源碼下載】
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)動態(tài)設(shè)置頁面標(biāo)題的方法,涉及微信小程序button組件事件綁定及頁面元素屬性動態(tài)設(shè)置相關(guān)實(shí)現(xiàn)技巧,并附帶完整源碼供讀者下載參考,需要的朋友可以參考下2017-11-11JavaScript實(shí)現(xiàn)網(wǎng)頁版簡易計(jì)算器功能
這篇文章主要介紹了JavaScript實(shí)現(xiàn)網(wǎng)頁版簡易計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-07-07如何利用Three.js實(shí)現(xiàn)web端顯示點(diǎn)云數(shù)據(jù)
這篇文章主要給大家介紹了關(guān)于如何利用Three.js實(shí)現(xiàn)web端顯示點(diǎn)云數(shù)據(jù)的相關(guān)資料,最近在項(xiàng)目中遇到需求,需要在web端顯示點(diǎn)云數(shù)據(jù),將我的實(shí)現(xiàn)步驟介紹在這里供大家參考,需要的朋友可以參考下2023-11-11javascript模擬的Ping效果代碼 (Web Ping)
JS雖然發(fā)送不了真正Ping的ICMP數(shù)據(jù)包,但Ping的本質(zhì)仍然是請求/回復(fù)的時間差,HTTP自然可以實(shí)現(xiàn)此功能.2011-03-03打印Proxy對象和ref對象的包實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了打印Proxy對象和ref對象的包實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11一文秒懂JavaScript構(gòu)造函數(shù)、實(shí)例、原型對象以及原型鏈
這篇文章主要介紹了一文秒懂JavaScript構(gòu)造函數(shù)、實(shí)例、原型對象以及原型鏈的相關(guān)知識,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08js點(diǎn)擊文本框后才加載驗(yàn)證碼實(shí)例代碼
這篇文章是一段關(guān)于js點(diǎn)擊文本框后才加載驗(yàn)證碼實(shí)例代碼,而不是直接顯示驗(yàn)證碼,感興趣的小伙伴們可以參考一下2015-10-10深入淺析JavaScript中prototype和proto的關(guān)系
prototype,每一個函數(shù)對象都有一個顯示的prototype屬性,而proto每個對象都有一個名為_proto_內(nèi)部隱藏屬性。本文給大家介紹JavaScript中prototype和proto的關(guān)系,需要的朋友參考下2015-11-11