JavaScript作用域

作用域

     在深入學習JavaScript作用域之前,首先要了解一下,究竟什麼是作用域。幾乎所有的編程語言都有作用域的概念,簡單的說,作用域就是變量與函數的可訪問範圍,即作用域控制着變量與函數的可見性和生命週期。

    我們先了解一下JavaScript的工作原理,引擎,編譯器,作用域三者是如何協同工作來完成javascript代碼的執行的。

    引擎:從頭到尾負責整個JavaScript程序的編譯及執行過程。

    編譯器:負責詞法分析及代碼生成

    作用域:負責收集並維護由所有聲明的變量組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些變量的訪問權限。

    我們看下最簡單的var index = 10;瞭解一下引擎、編譯器和作用域是如何協同工作的。    

    JS會將其看成是兩個聲明,第一個是定義聲明:編譯器在編譯階段執行。第二個是賦值聲明:由引擎在運行時執行。
因此可以分解爲:
[javascript] view plain copy
  1. var index;  
  2. index = 10;  
    首先遇到var index,"編譯器"會詢問"作用域":當前的作用域中是否有index,如果是,那麼"編譯器"會忽略這個聲明,繼續進行編譯;如果否,那麼它會要求“作用域”在當前的作用域聲明一個新的變量,並命名爲index.
    然後,"引擎"處理index = 10時,首先會詢問"作用域":當前的作用域中是否存在一個index的變量,如果是,那麼引擎就會使用這個變量,如果否,那麼"引擎"會繼續查找該變量。如果"引擎"最終找到了index變量,那麼就將10賦值給它,否則"引擎"就會拋出 一個異常(作用域鏈)。

    總結一下變量賦值操作過程,即:首先編譯器會在當前作用域中聲明一個變量(如果之前沒有聲明過),然後在運行時,引擎會在作用域中查找該變量,如果能夠找到就對它賦值,否則就拋出異常。
    此處需要注意的是:編譯階段是在當前的作用域中聲明變量,而引擎查找時,是在整個作用域中查找該變量。
    

    在上面的變量聲明的執行時,我們提到了引擎在作用域中查找變量的問題,我們將此分爲兩種方式,一種爲LHS查詢,一種爲RHS查詢,在上面的例子中,引擎在執行index = 10時,進行的是LHS查詢。

    那麼究竟何爲LHS查詢和RHS查詢呢,簡單的,當變量出現在左側時,執行的是LHS查詢(L可以認爲是Left)。除了LHS查詢,剩下的就是RHS查詢。作進一步說明,LHS查詢是企圖找到變量的容器本身,即去尋找賦值操作的目標,而RHS查詢是找賦值操作的源頭,即獲取變量的值。 

    舉例說明:

[javascript] view plain copy
  1. index = 10;   
  2. console.log(index);  
     在執行index = 10時,引擎會進行LHS查詢,去尋找index變量的容器,目的是找到賦值操作的目標。而在執行console.log(index)時,引擎會進行RHS查詢,目的是去找到index的值。我們之所以進行執行的區分,是因爲如果index變量沒有聲明的情況下,這兩種查詢方式的結果是完全不同的。

    作用域嵌套 引擎從當前的執行作用域開始查找變量,如果找不到,就向上一級繼續查找,直至到最外層的全局作用域鏈,不管最終是否找到了變量,查找過程到到此結束。

    如下:在執行index = 15時,引擎首先在當前作用域fun函數中查找index變量,沒有找到,那麼繼續向上查找,在par函數中也沒有找到,那麼繼續向上一級查找,最後在全局作用域中找到了該index.

[javascript] view plain copy
  1. <script>  
  2.     var index = 10;  
  3.     function par(){  
  4.          function fun(){  
  5.             index = 15;  
  6.           }  
  7.           fun();  
  8.     }      
  9.     par();  
  10. </script>  

    當引擎進行RHS查詢時,如果查詢到作用域鏈的頂層(全局作用域)依舊未找到index變量,那麼引擎就會拋出一個ReferenceError異常。

    當引擎進行LHS查詢,在全局作用域中也未能找到目標變量(本例中的index),在非嚴格模式下,會在全局作用域中創建一個該名稱的變量。而在嚴格模式下,會同RHS查詢一樣,拋出一個ReferenceError異常。

    作用域查找會在找到第一個匹配的標識符(變量)時停止,在多層嵌套作用域中可以定義同名的標識符,這也稱之爲"遮蔽效應",如上面的代碼中,如果fun函數中定義了index變量,那麼在fun中對index的賦值操作不會影響到全局變量中的index.因爲作用域查找始終是從運行時所處的最內部的作用域開始,逐級向上查找,直到找到匹配的標識符爲止。

     詞法作用域是由寫代碼時將變量和函數寫在哪裏決定的,而不是由其調用的位置決定,JS提供了兩種機制修改詞法作用域,即:width和eval,鑑於這兩種機制都會導致性能的降低,在此不多作介紹,儘量避免使用即可。

     初學者或多或少都會遇到一個問題:命名衝突。

     命名衝突會導致變量的值被意外覆蓋。而這並非是我們想看到的。那麼如何規避衝突呢?

     1.全局命名空間(類似於jQuery的實現)

     如:我們在全局作用域重視聲明瞭一個名字足夠獨特的變量,通常是一個變量,如下面的carousel_yve,這個對象被稱爲庫的命名空間,所有需要暴露給外界的功能都會稱爲這個對象的屬性(如:index、defaults、init),避免將自己的標識符暴露在頂級的詞法作用域中。

[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.     var carousel_yve = {  
  3.         index: 0,  
  4.         defaults: { width: "1200px",  
  5.                     height: "500px"},  
  6.         init: function(){  
  7.             console.log(this.defaults);  
  8.         }  
  9.     }  
  10.     carousel_yve.init(); //Object {width: "1200px", height: "500px"}  
  11.     console.log(carousel_yve.index); //10  
  12. </script>  

     2.模塊模式

     模塊模式分爲兩種,一種是每次調用都會創建一個新的模塊實例,另一種是單例模式,即只會創建一個實例。

     如:    

[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.     function carousel_yve(){  
  3.         var index = 0;  
  4.         var defaults = {width: "1200px",  
  5.                         height: "500px"};  
  6.         function init(){  
  7.             console.log(defaults);  
  8.         }  
  9.         function doSomething(){  
  10.             console.log(index);  
  11.         }  
  12.         return {  
  13.             init: init,  
  14.             doSomething: doSomething  
  15.         }  
  16.     }  
  17.     var example = carousel_yve();  
  18.     example.init(); //Object {width: "1200px", height: "500px"}  
  19.     example.doSomething(); //0  
  20. </script>  
    carousel_yve是一個函數,通過對它的調用來創建一個模塊實例。每次調用都會生成一個實例。

    我們再來看一下單例模式:    

[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.     var example = (function carousel_yve(){  
  3.         var index = 0;  
  4.         var defaults = {width: "1200px",  
  5.                         height: "500px"};  
  6.         function init(){  
  7.             console.log(defaults);  
  8.         }  
  9.         function doSomething(){  
  10.             console.log(index);  
  11.         }  
  12.         return {  
  13.             init: init,  
  14.             doSomething: doSomething  
  15.         }  
  16.     })();  
  17.     example.init(); //Object {width: "1200px", height: "500px"}  
  18.     example.doSomething(); //0  
  19. </script>  
    我們將先前的模塊函數換成了IIFE,即:立即調用。

    模塊模式需要具備兩個條件:

    1.必須有外部的封閉函數,該函數必須至少被調用一次。

    2.封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,並且可以訪問或者修改私有的狀態。

    鑑於本文的目的是研究javaScript的作用域問題,因此對於模塊模式不再做更多的擴展說明。

    關於IIFE,有時我們想對外隱藏時,也可以簡單的使用此方式。通過這樣的方式,避免污染所在的作用域。   

[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.     (function carousel_yve(){  
  3.         var index = 10;  
  4.         var defaults = {width: "1200px", height: "500px;"};  
  5.         var obj = document.getElementById("btn");  
  6.         obj.addEventListener("click"function(){/*code*/}, false);  
  7.         //code……  
  8.     })();  
  9. </script>  

     包裝函數的聲明以(function開始,而不是以function開始,看起來區別很小,但是實際上卻完全不一樣,因爲(function開始會當做函數表達式,而function開頭是作爲標準的函數聲明。而函數聲明和函數表達式最重要的區別在於它們的名稱標識符被綁定在何處。上面的代碼中carousel_yve被綁定在自身的函數中,而不是所在的作用域中,其只能在自身函數的內部被訪問。

     此外,對於匿名函數和具名函數還要做一點說明。

    JavaScript中,函數表達式允許匿名,但是函數聲明不允許省略函數名,即不允許匿名。儘管匿名函數使用起來簡單快捷,但是匿名函數的幾個缺點需要考慮:

    1.匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試困難。

    2.如果沒有函數名,當函數需要調用自身時,只能使用過期的arguments.callee引用,比如在遞歸中。

    3.匿名函數省略了對於代碼可讀性/可理解性很重要的函數名,一個描述性的名稱可以防止代碼不言自明。(代碼即註釋)

    匿名或具名並不會影響函數的功能,因此始終給函數表達式命名是值得推崇的。如我們經常使用的setTimeout、setInterval中。給回調函數命一個形象的名字將使代碼的可讀性更強。  

提升

    我們前面說過JavaScript運行代碼,分爲兩步,第一步:編譯,第二步:執行。
    在編譯階段,我們首先會提升變量聲明。舉例說明:    
[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.     console.log(a);   
  3.     var a = 2;  
  4. </script>  
    這兒是會拋出reference error呢還是Undefined呢?結果是Undefined,原因是因爲,作用域會進行提升操作,上面這段代碼實際上的處理流程是下面這樣子的。
[javascript] view plain copy
  1. var a;  
  2. console.log(a);  
  3. a = 2;  
    在執行console.log(a)時,a已經被聲明,僅僅是未賦值,因此結果是undefined,而非是拋出reference error.
    想想,如果是下面這個樣子的呢。結果又是什麼?    
[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.     console.log(a);  
  3.     a = 2;  
  4. </script>  
    這個地方輸出的又是什麼呢?結果是reference error,想一想原因是什麼。其實很好理解,這段代碼中,有對a的賦值,但是並沒有去聲明a,儘管在執行賦值時,引擎查在作用域中找不到a,會在全局的作用域中創建一個a,但是因爲a沒有聲明,所以在編譯時,不會被提升,在執行console.log(a)時,進行的是RHS查詢,在頂級作用域中查找不到a,拋出reference error的異常。
    正因爲這些差別,無論是全局作用域中,還是局部作用域中,希望大家都是使用var 去定義變量,而不是省略var,還口口聲聲說javascript是弱語言,有沒有var都一樣。很明顯,有很多區別,在函數作用域裏不使用var很有可能會無意中改變了全局變量的值。
    關於提升,還有一點需要說明的是:變量聲明和函數聲明都會被提升,但是函數優先
    舉例說明:    
[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.     example();  
  3.     var example = function(){  
  4.         console.log(10);  
  5.     };  
  6.     function example(){  
  7.         console.log(20);  
  8.     }  
  9. </script>  
    此處的輸出結果是20,而不是10。原因就是因爲函數優先,上面的代碼,實際的順序爲:    
[javascript] view plain copy
  1.        function example(){  
  2.     console.log(20);  
  3. }  
  4.        example();  
  5. example = function(){  
  6.     console.log(10);  
  7. };  
     函數聲明首先被提升,即function example()會被提升到第一步,第二步是var example,但是因爲作用域中已經有了example聲明,屬於重複聲明,被忽略。因此引擎正在理解的代碼如上所示,這就是爲什麼輸出的是20,而並非是10.
    值得注意的是:
    var的重複聲明會被忽略,但是函數的重複聲明會覆蓋,如下:    
[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.     example();  
  3.     var example = function(){  
  4.         console.log(10);  
  5.     };  
  6.     function example(){  
  7.         console.log(20);  
  8.     };  
  9.     function example(){  
  10.         console.log(30)  
  11.     }  
  12. </script>  
    輸出的結果是30,而不是20,因此請記住這個結論:
    函數聲明和變量聲明都會被提升,但是首先是函數被提升,然後纔是變量,重複的var聲明會被忽略,但是重複的函數聲明會覆蓋。
   另外一個需要注意的地方是:儘可能避免再塊內部聲明函數,至於爲何這樣說,我們來看一個例子:
[javascript] view plain copy
  1. <script type = "text/javascript">   
  2.     var a = true;  
  3.     if(a == true){  
  4.         function example(){  
  5.             console.log(10);  
  6.         };  
  7.     }else{  
  8.         function example(){  
  9.             console.log(20);  
  10.         }}   example();  
    你的本意是想當a爲true的時候,輸出10,而a爲false時,輸出20;很遺憾的是並非是你想的那樣,對於這個例子,火狐的輸出結果是10,而谷歌是20。顯然,引擎對其的處理有所不同。對此,建議您不要這樣使用。
    最後再簡單說下閉包的問題,關於閉包,之前已經寫過一篇博文介紹過,這裏再說明一次。
    閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數
    說一個最典型的問題,下面這段代碼是今天一個初學JS的朋友問我的,相信很多初學者都有遇到過這個問題。   
[html] view plain copy
  1. <html lang="en">  
  2. <head>  
  3.   <meta charset="UTF-8" />  
  4.   <title>Document</title>  
  5.   <style>  
  6.     #ullist li { display: block;width: 40px; height: 40px;   
  7.                 border:1px solid #ccc; text-align: center;  
  8.                 line-height: 40px; cursor: pointer; float: left;   
  9.                 margin:10px;}  
  10.   </style>  
  11. </head>  
  12. <body>  
  13.   <ul id="ullist">  
  14.     <li id="li1">1</li>  
  15.     <li id="li2">2</li>  
  16.     <li id="li3">3</li>  
  17.     <li id="li4">4</li>  
  18.     <li id="li5">5</li>  
  19.   </ul>  
  20. </body>  
  21. <script>  
  22.   window.onload=function(){   
  23.     var ullist=document.getElementById("ullist");  
  24.     var listE=document.getElementsByTagName("li");  
  25.     for (var i=0; i<listE.length; i++){  
  26.       listE[i].onclick = function(i){  
  27.         alert(listE[i].innerHTML);  
  28.     };      
  29.   };  
  30.  }  
  31. </script>  
  32. </html>  
    很顯然,他的目的是點擊每一個li時,彈出對應的內容,但是結果卻並非如此,並且控制檯中還會報錯。這是爲什麼呢?事實上,當你點擊時,i的值已經變成了listE.length;而listE[listE.length]是不存在的。JS中for並非是一個塊級作用域,因此i其實是定義在外部的一個變量,for中的函數共用同一個i.
    我們將JS的代碼改一改,就可以得到我們想要的結果,如下:
[javascript] view plain copy
  1. <script type = "text/javascript">  
  2.   window.οnlοad=function(){     
  3.     var ullist=document.getElementById("ullist");  
  4.     var listE=document.getElementsByTagName("li");  
  5.     for (var i=0; i<listE.length; i++){  
  6.       listE[i].onclick = (function(i){  
  7.         return function(){  
  8.           alert(listE[i].innerHTML);  
  9.         }  
  10.       })(i);  
  11.     };      
  12.   };  
  13. </script>  
    除了這個方法以外,還可以使用ES6中的let聲明for中的i,但是這需要支持ES6的瀏覽器。
    更多關於閉包的內容可以查看本人先前的博客《JS閉包與變量》。
    此篇博文花費時間較長,如果能爲您更一步理解JS作用域提供了一點點的幫助,也是值得的。

發佈了13 篇原創文章 · 獲贊 34 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章