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

什麼是做用域?chrome


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

 

什麼是做用域鏈?安全


       做用域鏈是做用域規則的實現,經過做用域鏈的實現,變量在它的做用域內可被訪問,函數在它的做用域內可被調用。性能優化

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

 

做用域鏈的造成?app


       咱們從一段代碼的執行來看做用域鏈的造成過程。函數

複製代碼
 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.閉包的注意事項

       因爲閉包中的變量不會像其餘正常變量那種被垃圾回收,而是一直存在內存中,因此大量使用閉包可能會形成性能問題。

相關文章
相關標籤/搜索