JavaScript從作用域到閉包

weixin_34026276發表於2017-11-26

    

作用域(scope)

全域性作用域和區域性作用域

通常來講這塊是全域性變數與區域性變數的區分。

全域性作用域:最外層函式和在最外層函式外面定義的變數擁有全域性作用域。

  1)最外層函式和在最外層函式外面定義的變數擁有全域性作用域

  2)所有末定義直接賦值的變數自動宣告為擁有全域性作用域,即沒有用var宣告的變數都是全域性變數,而且是頂層物件的屬性。

  3)所有window物件的屬性擁有全域性作用域

區域性作用域:和全域性作用域相反,區域性作用域一般只在固定的程式碼片段內可訪問到,最常見的例如函式內部,所以在一些地方也會看到有人把這種作用域稱為函式作用域。


 

塊作用域與函式作用域

函式作用域是相對塊作用域來進行解釋的,其和區域性作用域是一個意思。參考引文:JavaScript的作用域和塊級作用域概念理解

塊作用域:任何一對花括號{}中的語句集都屬於一個塊,在這之中定義的所有變數在程式碼塊外都是無效的,我們稱之為塊級作用域。

函式作用域:在函式中的引數和變數在函式外部是無法訪問的。JavaScript 的作用域是詞法性質的(lexically scoped)。這意味著,函式執行在定義它的作用域中,而不是在呼叫它的作用域中。下文會解釋。

 View Code

執行這段程式碼,會出現“use an undefined variable:j”的錯誤。可以看到,C語言擁有塊級作用域,因為j是在if的語句塊中定義的,因此,它在塊外是無法訪問的。

 View Code

執行這段程式碼,彈出"3",可見,在塊外,塊中定義的變數i仍然是可以訪問的。也就是說,JS並不支援塊級作用域,它只支援函式作用域,而且在一個函式中的任何位置定義的變數在該函式中的任何地方都是可見的。

 

作用域中的宣告提前

var scope="global";  //全域性變數function t(){  
    console.log(scope);  
    var scope="local" ;//區域性變數    console.log(scope);  
            }  
t();

 

(console.log()是控制檯的除錯工具,chrome叫檢查,有的瀏覽器叫審查元素,alert()彈窗會破壞頁面效果)

第一句輸出的是: "undefined",而不是 "global"

第二講輸出的是:"local"

第二個不用說,就是區域性變數輸出"local"。第一個之所以也是"local",是因為Js中的宣告提前,儘管在第4行才進行區域性變數的宣告與賦值,但其實是將第4行的宣告提前了,放在了函式體頂部,然後在第4行進行區域性變數的賦值。可以理解為下面這樣。

var scope="global";//全域性變數function t(){    var scope;//區域性變數宣告    console.log(scope);
    scope="local";//區域性變數賦值    console.log(scope);
}
t();

 


 

作用域鏈(Scope Chain)

當程式碼在一個環境中執行時,會建立變數物件的的一個作用域鏈(scope chain)。作用域鏈的用途,是保證對執行環境有權訪問的所有變數和函式的有序訪問。作用域鏈的前端,始終都是當前執行的程式碼所在環境的變數物件。如果這個環境是一個函式,則將其活動物件作為變數物件。參考引文:Js作用域與作用域鏈詳解,淺析作用域鏈–JS基礎核心之一

num="one";var a = 1;  
function t(){  //t函式的區域性作用域,可以訪問到a,b變數,但是訪問不到c變數
     var num="two"; 
     var b = 2;    function A(){ //A函式區域性作用域,可以訪問到a,b,c變數 
        var num="three"; //區域性變數與外部變數重名以區域性變數為主
        var c = 3;
        console.log(num); //three             }  
    function B(){  //B函式區域性作用域,可以訪問到a,b變數,訪問不到c變數
        console.log(num); //two             }  
    A();  
    B();  
}  
t();

當執行A時,將建立函式A的執行環境(呼叫物件),並將該物件置於連結串列開頭,然後將函式t的呼叫物件連結在之後,最後是全域性物件。然後從連結串列開頭尋找變數num。

即:A()->t()->window,所以num是”three";

但執行B()時,作用域鏈是: B()->t()->window,所以num是”two";

另外,有一個特殊的例子我覺得應該發一下。利用“JavaScript 的作用域是詞法性質的(lexically scoped)。這意味著,函式執行在定義它的作用域中,而不是在呼叫它的作用域中。” 這句話,解釋了下面的例子。

var x = 10;function a() {
console.log(x);
}function b () {var x = 5;
a();
}

b();//輸出為10

雖然b函式呼叫了a,但是a定義在全域性作用域下,同樣也是執行在全域性作用域下的,所以其內部的變數x,向上尋找到了全域性變數x=10;所以b函式的輸出為10;


經典案例

下面是一個經典的事件繫結例子:

<div id = "test">
    <p>欄目1</p>
    <p>欄目2</p>
    <p>欄目3</p>
    <p>欄目4</p>
</div>
 </body>
<script type="text/javascript">    
function bindClick(){    var allP = document.getElementById("test").getElementsByTagName("p"),
    i=0,
    len = allP.length;        
    for( ;i<len;i++){
    allP[i].onclick = function(){
        alert("you click the "+i+" P tag!");//you click the 4 P tag!        }
    }
}
bindClick();//執行函式,繫結點選事件</script>

上面的程式碼給P標籤新增點選事件,但是不管我們點選哪一個p標籤,我們獲取到的結果都是“you click the 4 P tag!”。

我們可以把上述的JS程式碼給分解一下,讓我們看起來更容易理解,如下所示。前面使用一個匿名函式作為click事件的回撥函式,這裡使用的一個非匿名函式,作為回撥,完全相同的效果。

function bindClick(){    var allP = document.getElementById("test").getElementsByTagName("p"),
    i=0,
    len = allP.length;    for( ;i<len;i++){
    allP[i].onclick = AlertP;
    }    function AlertP(){
    alert("you click the "+i+" P tag!");
    }
}
bindClick();//執行函式,繫結點選事件

這裡應該沒有什麼問題吧,前面使用一個匿名函式作為click事件的回撥函式,這裡使用的一個非匿名函式,作為回撥,完全相同的效果。也可以做下測試哦。

理解上面的說法了,那麼就可以很簡單的理解,為什麼我們之前的程式碼,會得到一個相同的結果了。首先看一下for迴圈中,這裡我們只是對每一個匹配的元素新增了一個click的回撥函式,並且回撥函式都是AlertP函式。這裡當為每一個元素新增成功click之後,i的值,就變成了匹配元素的個數,也就是i=len,而當我們觸發這個事件時,也就是當我們點選相應的元素時,我們期待的是,提示出我們點選的元素是排列在第幾行。click事件觸發時,執行回撥函式AlertP但是當執行到這裡的時候,發現alert方法中,有一個變數是未知的,並且在AlertP的區域性作用域中,也沒有查詢到相應的變數,那麼按照作用域鏈的查詢方式,就會向父級作用域去查詢,這裡的父級作用域中,確實是有變數i的,而i的值,卻是經過for迴圈之後的值i=len。所以也就出現了我們最初看到的效果。

解決辦法如下所示:

 allP = document.getElementById("test").getElementsByTagName("p"=0=( ;i<len;i++= "you click the "+i+" P tag!"

這裡,objiAlertP函式內部,就是區域性變數了。click事件的回撥函式,雖然依舊沒有變數i的值,但是其父作用域AlertP的內部,卻是有的,所以能正常的顯示了,這裡AlertP我放在了bindClick的內部,只是因為這樣可以減少必要的全域性函式,放到全域性也不影響的。

這裡是新增了一個函式進行繫結,如果我不想新增函式呢,當然也可以實現了,這裡就要說到自執行函式了。

 

函式宣告與賦值

宣告式函式、賦值式函式與匿名函式

匿名函式:function () {}; 使用function關鍵字宣告一個函式,但未給函式命名,所以叫匿名函式,匿名函式有很多作用,賦予一個變數則建立函式,賦予一個事件則成為事件處理程式或建立閉包等等。下文會講到。

JS中的函式定義分為兩種:宣告式函式與賦值式函式。

<script type="text/javascript">Fn(); //執行結果:"執行了宣告式函式",在預編譯期宣告函式及被處理了,所以即使Fn()呼叫函式放在宣告函式前也能執行。function Fn(){ //宣告式函式alert("執行了宣告式函式");
}</script>
<script type="text/javascript">Fn(); //執行結果:"Fn is not a function"var Fn = function(){ //賦值式函式alert("執行了賦值式函式");
}</script>

JS的解析過程分為兩個階段:預編譯期(預處理)與執行期。
預編譯期JS會對本程式碼塊中的所有宣告的變數和函式進行處理(類似與C語言的編譯),此時處理函式的只是宣告式函式,而且變數也只是進行了宣告(宣告提前)但未進行初始化以及賦值。所以才會出現上面兩種情況。

當正常情況,函式呼叫在宣告之後,同名函式會覆蓋前者。

<script type="text/javascript">function Fn(){ //宣告式函式alert("執行了宣告式函式");
}var Fn = function(){ //賦值式函式alert("執行了賦值式函式");
}
Fn();//執行結果:"執行了賦值式函式",同名函式後者會覆蓋前者</script>

 同理當提前呼叫宣告函式時,也存在同名函式覆蓋的情況。

<script type="text/javascript">Fn(); //執行結果:"執行了函式2",同名函式後者會覆蓋前者function Fn(){ //函式1alert("執行了函式1");
}function Fn(){ //函式2alert("執行了函式2");
}</script>

 

程式碼塊

JavaScript中的程式碼塊是指由<script>標籤分割的程式碼段。JS是按照程式碼塊來進行編譯和執行的,程式碼塊間相互獨立,但變數和方法共享。如下:

<script type="text/javascript">//程式碼塊一var test1 = "我是程式碼塊一test1";
alert(str);//因為沒有定義str,所以瀏覽器會出錯,下面的不能執行alert("我是程式碼塊一");//沒有執行到這裡var test2 = "我是程式碼塊一test2";//沒有執行到這裡但是預編譯環節宣告提前了,所以有變數但是沒賦值</script>
<script type="text/javascript">//程式碼塊二alert("我是程式碼塊二"); //這裡有執行到alert(test1); //彈出"我是程式碼塊一test1"alert(test2); //彈出"undefined"</script>

上面的程式碼中程式碼塊一中執行報錯,但不影響程式碼塊二的執行,這就是程式碼塊間的獨立性,而程式碼塊二中能呼叫到程式碼一中的變數,則是塊間共享性。

但是當第一個程式碼塊報錯停止後,並不影響下一個程式碼塊執行。當然在下面的例子中,雖然程式碼塊二中的函式宣告預編譯了,但是在程式碼塊1中的函式出現Fn函式為定義錯誤(瀏覽器報錯,並不是宣告未賦值的undefined),說明程式碼塊1完全執行後才執行程式碼塊2。

<script type="text/javascript">//程式碼塊1Fn(); //瀏覽器報錯:"undefined",停止程式碼塊1執行alert("執行了程式碼塊1");//未執行</script>
<script type="text/javascript">//程式碼塊2alert("執行了程式碼塊2");//執行彈框效果function Fn(){ //函式1alert("執行了函式1");
}</script>

所以js函式解析順序如下:
step 1. 讀入第一個程式碼塊。
step 2. 做語法分析,有錯則報語法錯誤(比如括號不匹配等),並跳轉到step5。
step 3. 對var變數和function定義做“預編譯處理”(永遠不會報錯的,因為只解析正確的宣告)。
step 4. 執行程式碼段,有錯則報錯(比如變數未定義)。
step 5. 如果還有下一個程式碼段,則讀入下一個程式碼段,重複step2。
step6. 結束。

:需要在頁面元素渲染前執行的js程式碼應該放在<body>前面的<script>代 碼塊中,而需要在頁面元素載入完後的js放在</body>元素後面,body標籤的onload事件是在最後執行的。

<script type="text/javascript">alert("first");function Fn(){
alert("third");
}</script>
<body onload="Fn()">
</body>
<script type="text/javascript">alert("second");</script>

 

自執行函式

也就是在函式名後新增括號,函式就會自執行。在繫結事件時,像我這樣的初學者有時會犯如下的錯誤,window.onclick = ab();這樣函式ab一開始就會執行。正確的做法應該將ab後的括號去掉。而這種加括號的做法其實是把ab函式執行的結果賦值給點選事件。

下面兩個例子清楚地反映了函式賦值後的情況。

1:

function ab () {    var i=0;
    alert("ab");    return i;
}var c=ab();//執行ab函式alert(typeof c+"      "+c);//number  0

2:

function ab () {    var i=0;
    alert("ab");    return i;
}var c=ab;//只賦值alert(typeof c+"      "+c);//function  function ab () {var i=0;alert("ab");return i;}

注:但是這個函式必須是函式表示式(諸如上文提到的賦值式函式),不能是函式宣告。詳細請看:js立即執行函式:(function(){...})()與(function(){...}())

文中主要講到匿名函式的自執行方法,即在function前面加!、+、 -甚至是逗號等到都可以起到函式定義後立即執行的效果,而()、!、+、-、=等運算子,都將函式宣告轉換成函式表示式,消除了javascript引擎識別函式表示式和函式宣告的歧義,告訴javascript引擎這是一個函式表示式,不是函式宣告,可以在後面加括號,並立即執行函式的程式碼(jq使用的就是這種方法)。舉例如下所示。

(function(a){
    console.log(a);   //firebug輸出123,使用()運算子})(123);
  
(function(a){
    console.log(a);   //firebug輸出1234,使用()運算子}(1234));  
!function(a){
    console.log(a);   //firebug輸出12345,使用!運算子}(12345);  
+function(a){
    console.log(a);   //firebug輸出123456,使用+運算子}(123456);  
-function(a){
    console.log(a);   //firebug輸出1234567,使用-運算子}(1234567);  
var fn=function(a){
    console.log(a);   //firebug輸出12345678,使用=運算子}(12345678)

其作用就是:實現塊作用域。

javascript中沒用私有作用域的概念,如果在多人開發的專案上,你在全域性或區域性作用域中宣告瞭一些變數,可能會被其他人不小心用同名的變數給覆蓋掉,根據javascript函式作用域鏈的特性,使用這種技術可以模仿一個私有作用域,用匿名函式作為一個“容器”,“容器”內部可以訪問外部的變數,而外部環境不能訪問“容器”內部的變數,所以( function(){…} )()內部定義的變數不會和外部的變數發生衝突,俗稱“匿名包裹器”或“名稱空間”。程式碼如下:

function test(){ 
(function (){ 
for(var i=0;i<4;i++){ 
} 
})(); 
alert(i); //瀏覽器錯誤:i is not defined} 
test();

 可以對比最開始介紹作用域時候的程式碼。

 

閉包(Closure)

閉包對於初學者來說很難,需要學習很多很多才能領會,所以也是先把作用域鏈和匿名函式的知識作為鋪墊。我這裡的閉包內容屬於基礎篇,以後可能會貼一些更為核心的內容。我這裡參照了大神們的講解來說。參考引文:學習Javascript閉包(Closure),JavaScript 匿名函式(anonymous function)與閉包(closure),淺析作用域鏈–JS基礎核心之一

閉包是能夠讀取其他函式內部變數的函式,所以在本質上,閉包將函式內部和函式外部連線起來的一座橋樑。

閉包是在函式執行結束,作用域鏈將函式彈出之後,函式內部的一些變數或者方法,還可以通過其他的方法引用。

兩個用處:一個是可以讀取函式內部的變數,另一個就是讓這些變數的值始終保持在記憶體中。

為了幫助理解,我找了幾個例子:

1.(阮一峰老師的講解)

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999  nAdd();
  result(); // 1000

在這段程式碼中,result實際上就是閉包f2函式。它一共執行了兩次,第一次的值是999,第二次的值是1000。這證明了,函式f1中的區域性變數n一直儲存在記憶體中,並沒有在f1呼叫後被自動清除。

為什麼會這樣呢?原因就在於f1是f2的父函式,而f2被賦給了一個全域性變數,這導致f2始終在記憶體中,而f2的存在依賴於f1,因此f1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。

這段程式碼中另一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全域性變數,而不是區域性變數。其次,nAdd的值是一個匿名函式(anonymous function),而這個匿名函式本身也是一個閉包,所以nAdd相當於是一個setter,可以在函式外部對函式內部的區域性變數進行操作。

2.(某大神)

function foo() { 
var a = 10; 
function bar() { 
a *= 2; 
return a; 
} 
return bar; 
} 
var baz = foo(); 
alert(baz()); //20alert(baz()); //40    alert(baz()); //80var blat = foo(); 
alert(blat()); //20

現在可以從外部訪問 a; 
a 是執行在定義它的 foo 中,而不是執行在呼叫 foo 的作用域中。 只要 bar 被定義在 foo 中,它就能訪問 foo 中定義的變數 a,即使 foo 的執行已經結束。也就是說,按理,"var baz = foo()" 執行後,foo 已經執行結束,a 應該不存在了,但之後再呼叫 baz 發現,a 依然存在。這就是 JavaScript 特色之一——執行在定義,而不是執行的呼叫。 
其中, "var baz = foo()" 是一個 bar 函式的引用;"var blat= foo()" 是另一個 bar 函式引用。 

    



本文轉自 sshpp 51CTO部落格,原文連結:http://blog.51cto.com/12902932/1924614,如需轉載請自行聯絡原作者

相關文章