前言
深入了解閉包和作用域鏈就需先了解函數預編譯的過程
一、預編譯
JavaScript:運行三部曲:
語法分析–預編譯–解釋執行
預編譯:
發生在函數執行的前一刻。
函數聲明整體提升,變量只聲明提升。
1.函數預編譯的過程:
1.創建AO對象Activation Object(執行期上下文,其作用就是我們理解的作用域,函數產生的執行空間庫)
2.找形參和變量聲明,將變量和形參名作為AO屬性名,值為undefined
3.將實參值與形參統一
4.找到函數聲明,將函數名作為屬性名,值為函數體。
例:
function test (a, b){ console.log(a); c = 0; var c; a = 3; b = 2; console.log(b); function b (){}; function d (){}; console.log(b); } test(1);
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
2.全局預編譯它和函數預編譯步驟一樣,但它創造的是GO(全局對象):
1.生成了一個 GO 的對象 Global Object(window 就是 GO)
2.找變量聲明…
3.找函數聲明…
任何全局變量都是 window 上的屬性
變量沒有聲明就賦值了,歸 window 所有,就是在 GO 里面預編譯。
例 :
function test(){ var a = b =123; console.log(window.b); } test(); 答案 a 是 undefined,b 是 123 先生成 GO{ b : 123 } 再有 AO{ a : undefined }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
想執行全局,先生成 GO,在執行 test 的前一刻生成 AO
函數里找變量,因為GO和AO有幾層嵌套關系,近的優先,從近的到遠的, AO里有就看 AO,AO 沒有才看 GO。所以函數局部變量和全局變量同名,函數內只會用局部。
二、作用域精講
作用域定義:變量(變量作用于又稱上下文)和函數生效(能被訪問)的區域
全局、局部變量
作用域的訪問順序:函數外面不能用函數里面的。里面的可以訪問外面的,外面的不能訪問里面的,彼此獨立的區間不能相互訪問。
1.[[scope]]: 每個 javascript 函數都是一個對象,對象中有些屬性我們可以訪問,但有些不可以,這些屬性僅供 javascript 引擎存取,[[scope]]就是其中一個。[[scope]]指的就是我們所說的作用域,其中存儲了運行期上下文的集合。
2.執行期上下文: 當函數在執行的前一刻,會創建一個稱為執行期上下文的內部對象(AO)。
一個執行期上下文定義了一個函數執行時的環境,函數每次執行時對應的執行上下文都是獨一無二的,所以多次調用一個函數會導致創建多個執行上下文,當函數執行完畢,執行上下文被銷毀。
3.作用域鏈:[[scope]]中所存儲的執行期上下文對象的集合(GO和AO),這個集合呈鏈式鏈接,我們把這種鏈式鏈接叫做作用域鏈。
4.查找變量: 在哪個函數里面查找變量,就從哪個函數作用域鏈的頂端依次向下查找(先查自己的AO,再查父級的AO,一直到最后的GO)。
函數類對象,我們能訪問 test.name
test.[[scope]]隱式屬性——作用域
作用域鏈圖解:
function a (){ function b (){ var bb = 234; aa = 0; } var aa = 123; b(); console.log(aa) } var glob = 100; a();
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
0 是最頂端,1 是次頂端,查找順序是從最頂端往下查

在全局預編譯中函數a定義時,它的[[scope]]屬性中有GO對象。

在函數a執行前先函數預編譯,創建自己的AO對象,并存儲在[[scope]]屬性上,與之前存儲的GO成鏈式。同時函數b被創建定義。

在b被創建時,它生成的[[scope]]屬性直接存儲了父級的[[scope]],它有了父級的AO和GO。

b函數執行前預編譯,生成自己的AO,存儲在[[scope]]屬性中。
詳解過程: 注意[[scope]]它是數組,存儲的都是引用值。
b 中 a 的 AO 與 a 的 AO,就是同一個 AO,b 只是引用了 a 的 AO,GO 也都是同一個。
function b(){}執行完,干掉的是 b 自己的 AO(銷毀執行期上下文)(去掉連接線),下次 function b 被執行時,產生的是新的 b 的 AO。b 執行完只會銷毀自己的 AO,不會銷毀 a 的 AO。會退回到b被定義時(仍有父級的AO和GO)。
function a(){}執行完,會把 a 自己的 AO 銷毀【也會把 function b的[[scope]]也銷毀】,只剩 GO(回歸到 a 被定義的時候),等下次 function a再次被執行時,會產生一個全新的 AO,里面有一個新的 b 函數。。。。。。周而復始。
思考一個問題:如果 function a 不被執行,下面的 function b 和 function c 都是看不到的(也不會被執行,被折疊)。只有 function a 被執行,才能執行 function a 里面的內容a();不執行,根本看不到 function a (){}里面的內容,但我們想在a函數外面調用b函數怎么辦呢,于是閉包出現了。
三、閉包
閉包的定義
當內部函數被保存到外部時,將會生成閉包。但凡是內部的函數被保存到外部,一定生成閉包。
閉包的問題:閉包會導致原有作用域鏈不釋放,作用域中的局部變量一直被使用著,導致該作用域釋放不掉,造成內存泄露(就是占有過多內存,導致內存越來越少,就像泄露了一樣)
例:
function a(){ function b(){ var b=456; console.log(a); console.log(b); } var a=123; return b; } var glob = a(); glob();
答案 123,456。
function a(){ }是在 return b 之后才執行完,才銷毀。而return b 把 b(包括 a 的 AO)保存到外部了(放在全局)當 a 執行完砍掉自己的 AO 時(砍掉對AO存儲地址的指針),因為b還保存著對a的AO的引用,所以內存清除機制不會清除掉a的AO, b 依然可以訪問到 a 的 AO。
閉包的作用:
1.實現共有變量
function test(){ var num=100; function a(){ num++; } function b(){ num--; } return [a,b]; } var myArr=test(); myArr[0](); myArr[1]();
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
答案 101 和 100。
思考過程:說明兩個用的是一個 AO。
myArr[0]是數組第一位的意思,即 a,myArr0;就是執行函數 a 的意思;
myArr[1]是數組第二位的意思,即 b,myArr1; 就是執行函數 b 的意思。
test doing test[[scope]] 0:testAO
1:GO
a defined a.[[scope]] 0 : testAO
1 : GO
b defined b.[[scope]] 0 : testAO
1 : GO
return[a, b]將 a 和 b 同時被定義的狀態被保存出來了
當執行 myArr0;時
a doing a.[[scope]] 0 : aAO
1 : testAO
2 : GO
當執行 myArr1;時
b doing b.[[scope]] 0 : bAO
1 : a 運行后的 testAO
2 : GO
a 運行后的 testAO, 與 a doing 里面的 testAO 一模一樣
a 和 b 連線的都是 test 環境,對應的一個閉包
2.可以做緩存(存儲結構)
function eater(){ var food=""; var obj={ eat : function (myFood){ console.log("i am eating"+food); food =""; }, push : function (myFood){ food = myFood; } } return obj; } var eater1 = eater(); eater1.push("banana"); eater1.eat();
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
答案 i am eating banana,eat 和 push 操作的是同一個 food
在 function eater(){里面的 food}就相當于一個隱式存儲的機構
obj 對象里面是可以有 function 方法的,也可以有屬性,方法就是函數的表現形式
3.可以實現封裝,屬性私有化
只能調用函數方法,不能修改函數的屬性。
4.模塊化開發,防止污染全局變量
藍藍設計建立了UI設計分享群,每天會分享國內外的一些優秀設計,如果有興趣的話,可以進入一起成長學習,請掃碼藍小助,報下信息,藍小助會請您入群。歡迎您加入噢~~希望得到建議咨詢、商務合作,也請與我們聯系。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
轉自:csdn
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務