圖解Javascript——作用域、作用域鏈、閉包


什么是作用域?


       作用域是一種規則,在代碼編譯階段就確定了,規定了變量與函數的可被訪問的范圍。全局變量擁有全局作用域,局部變量則擁有局部作用域。 js是一種沒有塊級作用域的語言(包括if、for等語句的花括號代碼塊或者單獨的花括號代碼塊都不能形成一個局部作用域),所以js的局部作用域的形成有且只有函數的花括號內定義的代碼塊形成的,既函數作用域。

 

什么是作用域鏈?


       作用域鏈是作用域規則的實現,通過作用域鏈的實現,變量在它的作用域內可被訪問,函數在它的作用域內可被調用。

作用域鏈是一個只能單向訪問的鏈表,這個鏈表上的每個節點就是執行上下文的變量對象(代碼執行時就是活動對象),單向鏈表的頭部(可被第一個訪問的節點)始終都是當前正在被調用執行的函數的變量對象(活動對象),尾部始終是全局活動對象。

 

作用域鏈的形成?


       我們從一段代碼的執行來看作用域鏈的形成過程。

 1 function fun01 () {
 2     console.log('i am fun01...');
 3     fun02();
 4 }
 5 
 6 function fun02 () {
 7     console.log('i am fun02...');
 8 }
 9 
10 fun01(); 

 

數據訪問流程


       如上圖,當程序訪問一個變量時,按照作用域鏈的單向訪問特性,首先在頭節點的AO中查找,沒有則到下一節點的AO查找,最多查找到尾節點(global AO)。在這個過程中找到了就找到了,沒找到就報錯undefined。

 

延長作用域鏈


       從上面作用域鏈的形成可以看出鏈上的每個節點是在函數被調用執行是向鏈頭unshift進當前函數的AO,而節點的形成還有一種方式就是“延長作用域鏈”,既在作用域鏈的頭部插入一個我們想要的對象作用域。延長作用域鏈有兩種方式:

1.with語句

1 function fun01 () {
2     with (document) {
3         console.log('I am fun01 and I am in document scope...')
4     }
5 }
6 
7 fun01();

 

 

 

2.try-catch語句的catch塊

1 function fun01 () {
2     try {
3         console.log('Some exceptions will happen...')
4     } catch (e) {
5         console.log(e)
6     }
7 }
8 
9 fun01();

 

 

ps:個人感覺with語句使用需求不多,try-catch的使用也是看需求的。個人對這兩種使用不多,但是在進行這部分整理過程中萌發了一點點在作用域鏈層面的不成熟的性能優化小建議。

 

由作用域鏈引發的關於性能優化的一點不成熟的小建議


1.減少變量的作用域鏈的訪問節點

       這里我們自定義一個名次叫做“查找距離”,表示程序訪問到一個非undefined變量在作用域鏈中經過的節點數。因為如果在當前節點沒有找到變量,就跳到下一個節點查找,還要進行判斷下一個節點中是否存在被查找變量。“查找距離”越長,要做的“跳”動作和“判斷”動作也就越多,資源開銷就越大,從而影響性能。這種性能帶來的差距可能少數的幾次變量查找操作不會帶來太多性能問題,但如果是多次進行變量查找,性能對比則比較明顯了。

 1 (function(){
 2     console.time()
 3     var find = 1      //這個find變量需要在4個作用域鏈節點進行查找
 4     function fun () {
 5         function funn () {
 6             var funnv = 1;
 7             var funnvv = 2;
 8             function funnn () {
 9                 var i = 0
10                 while(i <= 100000000){
11                     if(find){
12                         i++
13                     }
14                 }
15             }
16             funnn()
17         }
18         funn()
19     }
20     fun()
21     console.timeEnd()
22 })()

 
        

 

 1 (function(){
 2     console.time()
 3     function fun () {
 4         function funn () {
 5             var funnv = 1;
 6             var funnvv = 2;
 7             function funnn () {
 8                 var i = 0
 9                 var find = 1      //這個find變量只在當前節點進行查找
10                 while(i <= 100000000){
11                     if(find){
12                         i++
13                     }
14                 }
15             }
16             funnn()
17         }
18         funn()
19     }
20     fun()
21     console.timeEnd()
22 })()

 
        

 

       在mac pro的chrome瀏覽器下做實驗,進行1億次查找運算。

       實驗結果:前者運行5次平均耗時85.599ms,后者運行5次平均耗時63.127ms。

 

2.避免作用域鏈內節點AO上過多的變量定義

       過多的變量定義造成性能問題的原因主要是查找變量過程中的“判斷”操作開銷較大。我們使用with來進行性能對比。

 1 (function(){
 2     console.time()
 3     function fun () {
 4         function funn () {
 5             var funnv = 1;
 6             var funnvv = 2;
 7             function funnn () {
 8                 var i = 0
 9                 var find = 10
10                 with (document) {
11                     while(i <= 1000000){
12                         if(find){
13                             i++
14                         }
15                     }
16                 }
17             }
18             funnn()
19         }
20         funn()
21     }
22     fun()
23     console.timeEnd()
24 })()

 
        

 

       在mac pro的chrome瀏覽器下做實驗,進行100萬次查找運算,借助with使用document進行的延長作用域鏈,因為document下的變量屬性比較多,可以測試在多變量作用域鏈節點下進行查找的性能差異。

       實驗結果:5次平均耗時558.802ms,而如果刪掉with和document,5次平均耗時0.956ms。

       當然,這兩個實驗是在我們假設的極端環境下進行的,結果僅供參考!

 

關於閉包


1.什么是閉包?

       函數對象可以通過作用域鏈相互關聯起來,函數體內的數據(變量和函數聲明)都可以保存在函數作用域內,這種特性在計算機科學文獻中被稱為“閉包”。既函數體內的數據被隱藏於作用於鏈內,看起來像是函數將數據“包裹”了起來。從技術角度來說,js的函數都是閉包:函數都是對象,都關聯到作用域鏈,函數內數據都被保存在函數作用域內。

2.閉包的幾種實現方式

       實現方式就是函數A在函數B的內部進行定義了,並且當函數A在執行時,訪問了函數B內部的變量對象,那么B就是一個閉包。如下:

 

 

       如上兩圖所示,是在chrome瀏覽器下查看閉包的方法。兩種方式的共同點是都有一個外部函數outerFun(),都在外部函數內定義了內部函數innerFun(),內部函數都訪問了外部函數的數據。不同的是,第一種方式的innerFun()是在outerFun()內被調用的,既聲明和被調用均在同一個執行上下文內。而第二種方式的innerFun()則是在outerFun()外被調用的,既聲明和被調用不在同一個執行上下文。第二種方式恰好是js使用閉包常用的特性所在:通過閉包的這種特性,可以在其他執行上下文內訪問函數內部數據。

我們更常用的一種方式則是這樣的:

 1 //閉包實例
 2 function outerFun () {
 3     var outerV1 = 10
 4     function outerF1 () { 5 console.log('I am outerF1...') 6  } 7 8 function innerFun () { 9 var innerV1 = outerV1 10  outerF1() 11  } 12 return innerFun //return回innerFun()內部函數 13 } 14 var fn = outerFun() //接到return回的innerFun()函數 15 fn() //執行接到的內部函數innerFun()

此時它的作用域鏈是這樣的:

 

3.閉包的好處及使用場景

       js的垃圾回收機制可以粗略的概括為:如果當前執行上下文執行完畢,且上下文內的數據沒有其他引用,則執行上下文pop出call stack,其內數據等待被垃圾回收。而當我們在其他執行上下文通過閉包對執行完的上下文內數據仍然進行引用時,那么被引用的數據則不會被垃圾回收。就像上面代碼中的outerV1,放我們在全局上下文通過調用innerFun()仍然訪問引用outerV1時,那么outerFun執行完畢后,outerV1也不會被垃圾回收,而是保存在內存中。另外,outerV1看起來像不像一個outerFun的私有內部變量呢?除了innerFun()外,我們無法隨意訪問outerV1。所以,綜上所述,這樣閉包的使用情景可以總結為:

(1)進行變量持久化。

(2)使函數對象內有更好的封裝性,內部數據私有化。

進行變量持久化方面舉個栗子:

       我們假設一個需求時寫一個函數進行類似id自增或者計算函數被調用的功能,普通青年這樣寫:

1 var count = 0
2 function countFun () {
3     return count++
4 }

       這樣寫固然實現了功能,但是count被暴露在外,可能被其他代碼篡改。這個時候閉包青年就會這樣寫:

1 function countFun () {
2     var count = 0
3     return function(){ 4 return count++ 5  } 6 } 7 8 var a = countFun() 9 a()

 

       這樣count就不會被不小心篡改了,函數調用一次就count加一次1。而如果結合“函數每次被調用都會創建一個新的執行上下文”,這種count的安全性還有如下體現:

 1 function countFun () {
 2     var count = 0
 3     return { 4 count: function () { 5 count++ 6  }, 7 reset: function () { 8 count = 0 9  }, 10 printCount: function () { 11  console.log(count) 12  } 13  } 14 } 15 16 var a = countFun() 17 var b = countFun() 18 a.count() 19 a.count() 20 21 b.count() 22 b.reset() 23 24 a.printCount() //打印:2 因為a.count()被調用了兩次 25 b.printCount() //打印出:0 因為調用了b.reset()

       以上便是閉包提供的變量持久化和封裝性的體現。

 

4.閉包的注意事項

       由於閉包中的變量不會像其他正常變量那種被垃圾回收,而是一直存在內存中,所以大量使用閉包可能會造成性能問題。

 

我是Han,我的終極夢想不是世界和平,而是不勞而獲!thx!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM