JavaScript關(guān)于作用域、作用域鏈和閉包的理解

作用域

先來(lái)談?wù)勛兞康淖饔糜?br> 變量的作用域無(wú)非就是兩種:全局變量和局部變量。
全局作用域:
最外層函數(shù)定義的變量擁有全局作用域,即對(duì)任何內(nèi)部函數(shù)來(lái)說(shuō),都是可以訪問(wèn)的:

<script>
      var outerVar = "outer";
      function fn(){
         console.log(outerVar);
      }
      fn();//result:outer
   </script>

局部作用域:
和全局作用域相反,局部作用域一般只在固定的代碼片段內(nèi)可訪問(wèn)到,而對(duì)于函數(shù)外部是無(wú)法訪問(wèn)的,最常見(jiàn)的例如函數(shù)內(nèi)部

<script>
      function fn(){
         var innerVar = "inner";
      }
      fn();
      console.log(innerVar);// ReferenceError: innerVar is not defined
</script>

需要注意的是,函數(shù)內(nèi)部聲明變量的時(shí)候,一定要使用var命令。如果不用的話,你實(shí)際上聲明了一個(gè)全局變量!

   <script>
      function fn(){
         innerVar = "inner";
      }
      fn();
      console.log(innerVar);// result:inner
   </script>

再來(lái)看一個(gè)代碼:

   <script>
      var scope = "global";
      function fn(){
         console.log(scope);//result:undefined
         var scope = "local";
         console.log(scope);//result:local;
      }
      fn();
   </script>

很有趣吧,第一個(gè)輸出居然是undefined,原本以為它會(huì)訪問(wèn)外部的全局變量(scope=”global”),但是并沒(méi)有。這可以算是javascript的一個(gè)特點(diǎn),只要函數(shù)內(nèi)定義了一個(gè)局部變量,函數(shù)在解析的時(shí)候都會(huì)將這個(gè)變量“提前聲明”

   <script>
      var scope = "global";
      function fn(){
         var scope;//提前聲明了局部變量
         console.log(scope);//result:undefined
         scope = "local";
         console.log(scope);//result:local;
      }
      fn();
   </script>

然而,也不能因此草率地將局部作用域定義為:用var聲明的變量作用范圍起止于花括號(hào)之間。
javascript并沒(méi)有塊級(jí)作用域
那什么是塊級(jí)作用域?
像在C/C++中,花括號(hào)內(nèi)中的每一段代碼都具有各自的作用域,而且變量在聲明它們的代碼段之外是不可見(jiàn)的,比如下面的c語(yǔ)言代碼:

for(int i = 0; i < 10; i++){
//i的作用范圍只在這個(gè)for循環(huán)
}
printf("%d",&i);//error

但是javascript不同,并沒(méi)有所謂的塊級(jí)作用域,javascript的作用域是相對(duì)函數(shù)而言的,可以稱(chēng)為函數(shù)作用域:

   <script>
      for(var i = 1; i < 10; i++){
            //coding
      }
      console.log(i); //10  
   </script>

作用域鏈(Scope Chain)

那什么是作用域鏈?
我的理解就是,根據(jù)在內(nèi)部函數(shù)可以訪問(wèn)外部函數(shù)變量的這種機(jī)制,用鏈?zhǔn)讲檎覜Q定哪些數(shù)據(jù)能被內(nèi)部函數(shù)訪問(wèn)。
想要知道js怎么鏈?zhǔn)讲檎?,就得先了解js的執(zhí)行環(huán)境

執(zhí)行環(huán)境(execution context)

每個(gè)函數(shù)運(yùn)行時(shí)都會(huì)產(chǎn)生一個(gè)執(zhí)行環(huán)境,而這個(gè)執(zhí)行環(huán)境怎么表示呢?js為每一個(gè)執(zhí)行環(huán)境關(guān)聯(lián)了一個(gè)變量對(duì)象。環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對(duì)象中。
全局執(zhí)行環(huán)境是最外圍的執(zhí)行環(huán)境,全局執(zhí)行環(huán)境被認(rèn)為是window對(duì)象,因此所有的全局變量和函數(shù)都作為window對(duì)象的屬性和方法創(chuàng)建的。
js的執(zhí)行順序是根據(jù)函數(shù)的調(diào)用來(lái)決定的,當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),該函數(shù)環(huán)境的變量對(duì)象就被壓入一個(gè)環(huán)境棧中。而在函數(shù)執(zhí)行之后,棧將該函數(shù)的變量對(duì)象彈出,把控制權(quán)交給之前的執(zhí)行環(huán)境變量對(duì)象。
舉個(gè)例子:

   <script>
      var scope = "global"; 
      function fn1(){
         return scope; 
      }
      function fn2(){
         return scope;
      }
      fn1();
      fn2();
   </script>

上面代碼執(zhí)行情況演示:


了解了環(huán)境變量,再詳細(xì)講講作用域鏈。
當(dāng)某個(gè)函數(shù)第一次被調(diào)用時(shí),就會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境(execution context)以及相應(yīng)的作用域鏈,并把作用域鏈賦值給一個(gè)特殊的內(nèi)部屬性([scope])。然后使用this,arguments(arguments在全局環(huán)境中不存在)和其他命名參數(shù)的值來(lái)初始化函數(shù)的活動(dòng)對(duì)象(activation object)。當(dāng)前執(zhí)行環(huán)境的變量對(duì)象始終在作用域鏈的第0位。
以上面的代碼為例,當(dāng)?shù)谝淮握{(diào)用fn1()時(shí)的作用域鏈如下圖所示:
(因?yàn)閒n2()還沒(méi)有被調(diào)用,所以沒(méi)有fn2的執(zhí)行環(huán)境)



可以看到fn1活動(dòng)對(duì)象里并沒(méi)有scope變量,于是沿著作用域鏈(scope chain)向后尋找,結(jié)果在全局變量對(duì)象里找到了scope,所以就返回全局變量對(duì)象里的scope值。

標(biāo)識(shí)符解析是沿著作用域鏈一級(jí)一級(jí)地搜索標(biāo)識(shí)符地過(guò)程。搜索過(guò)程始終從作用域鏈地前端開(kāi)始,然后逐級(jí)向后回溯,直到找到標(biāo)識(shí)符為止(如果找不到標(biāo)識(shí)符,通常會(huì)導(dǎo)致錯(cuò)誤發(fā)生)—-《JavaScript高級(jí)程序設(shè)計(jì)》

那作用域鏈地作用僅僅只是為了搜索標(biāo)識(shí)符嗎?
再來(lái)看一段代碼:

   <script>
      function outer(){
         var scope = "outer";
         function inner(){
            return scope;
         }
         return inner;
      }
      var fn = outer();
      fn();
   </script>

outer()內(nèi)部返回了一個(gè)inner函數(shù),當(dāng)調(diào)用outer時(shí),inner函數(shù)的作用域鏈就已經(jīng)被初始化了(復(fù)制父函數(shù)的作用域鏈,再在前端插入自己的活動(dòng)對(duì)象),具體如下圖:


一般來(lái)說(shuō),當(dāng)某個(gè)環(huán)境中的所有代碼執(zhí)行完畢后,該環(huán)境被銷(xiāo)毀(彈出環(huán)境棧),保存在其中的所有變量和函數(shù)也隨之銷(xiāo)毀(全局執(zhí)行環(huán)境變量直到應(yīng)用程序退出,如網(wǎng)頁(yè)關(guān)閉才會(huì)被銷(xiāo)毀)
但是像上面那種有內(nèi)部函數(shù)的又有所不同,當(dāng)outer()函數(shù)執(zhí)行結(jié)束,執(zhí)行環(huán)境被銷(xiāo)毀,但是其關(guān)聯(lián)的活動(dòng)對(duì)象并沒(méi)有隨之銷(xiāo)毀,而是一直存在于內(nèi)存中,因?yàn)樵摶顒?dòng)對(duì)象被其內(nèi)部函數(shù)的作用域鏈所引用。
具體如下圖:
outer執(zhí)行結(jié)束,內(nèi)部函數(shù)開(kāi)始被調(diào)用
outer執(zhí)行環(huán)境等待被回收,outer的作用域鏈對(duì)全局變量對(duì)象和outer的活動(dòng)對(duì)象引用都斷了



像上面這種內(nèi)部函數(shù)的作用域鏈仍然保持著對(duì)父函數(shù)活動(dòng)對(duì)象的引用,就是閉包(closure)

閉包

閉包有兩個(gè)作用:
第一個(gè)就是可以讀取自身函數(shù)外部的變量(沿著作用域鏈尋找)
第二個(gè)就是讓這些外部變量始終保存在內(nèi)存中
關(guān)于第二點(diǎn),來(lái)看一下以下的代碼:

   <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){//注:i是outer()的局部變量
            result[i] = function(){
               return i;
            }
         }
         return result;//返回一個(gè)函數(shù)對(duì)象數(shù)組
         //這個(gè)時(shí)候會(huì)初始化result.length個(gè)關(guān)于內(nèi)部函數(shù)的作用域鏈
      }
      var fn = outer();
      console.log(fn[0]());//result:2
      console.log(fn[1]());//result:2
   </script>

返回結(jié)果很出乎意料吧,你肯定以為依次返回0,1,但事實(shí)并非如此
來(lái)看一下調(diào)用 fn[0]()的作用域鏈圖:

可以看到result[0]函數(shù)的活動(dòng)對(duì)象里并沒(méi)有定義i這個(gè)變量,于是沿著作用域鏈去找i變量,結(jié)果在父函數(shù)outer的活動(dòng)對(duì)象里找到變量i(值為2),而這個(gè)變量i是父函數(shù)執(zhí)行結(jié)束后將最終值保存在內(nèi)存里的結(jié)果。
由此也可以得出,js函數(shù)內(nèi)的變量值不是在編譯的時(shí)候就確定的,而是等在運(yùn)行時(shí)期再去尋找的。
那怎么才能讓result數(shù)組函數(shù)返回我們所期望的值呢?
看一下result的活動(dòng)對(duì)象里有一個(gè)arguments,arguments對(duì)象是一個(gè)參數(shù)的集合,是用來(lái)保存對(duì)象的。
那么我們就可以把i當(dāng)成參數(shù)傳進(jìn)去,這樣一調(diào)用函數(shù)生成的活動(dòng)對(duì)象內(nèi)的arguments就有當(dāng)前i的副本。
改進(jìn)之后:

   <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定義一個(gè)帶參函數(shù)
            function arg(num){
               return num;
            }
            //把i當(dāng)成參數(shù)傳進(jìn)去
            result[i] = arg(i);
         }
         return result;
      }
      var fn = outer();
      console.log(fn[0]);//result:0
      console.log(fn[1]);//result:1
   </script>

雖然的到了期望的結(jié)果,但是又有人問(wèn)這算閉包嗎?調(diào)用內(nèi)部函數(shù)的時(shí)候,父函數(shù)的環(huán)境變量還沒(méi)被銷(xiāo)毀呢,而且result返回的是一個(gè)整型數(shù)組,而不是一個(gè)函數(shù)數(shù)組!
確實(shí)如此,那就讓arg(num)函數(shù)內(nèi)部再定義一個(gè)內(nèi)部函數(shù)就好了:
這樣result返回的其實(shí)是innerarg()函數(shù)

   <script>
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定義一個(gè)帶參函數(shù)
            function arg(num){
               function innerarg(){
                  return num;
               }
               return innerarg;
            }
            //把i當(dāng)成參數(shù)傳進(jìn)去
            result[i] = arg(i);
         }
         return result;
      }
      var fn = outer();
      console.log(fn[0]());
      console.log(fn[1]());
   </script>

當(dāng)調(diào)用outer,for循環(huán)內(nèi)i=0時(shí)的作用域鏈圖如下:


由上圖可知,當(dāng)調(diào)用innerarg()時(shí),它會(huì)沿作用域鏈找到父函數(shù)arg()活動(dòng)對(duì)象里的arguments參數(shù)num=0.
上面代碼中,函數(shù)arg在outer函數(shù)內(nèi)預(yù)先被調(diào)用執(zhí)行了,對(duì)于這種方法,js有一種簡(jiǎn)潔的寫(xiě)法

    function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            //定義一個(gè)帶參函數(shù)
            result[i] = function(num){
               function innerarg(){
                  return num;
               }
               return innerarg;
            }(i);//預(yù)先執(zhí)行函數(shù)寫(xiě)法
            //把i當(dāng)成參數(shù)傳進(jìn)去
         }
         return result;
      }

關(guān)于this對(duì)象
關(guān)于閉包經(jīng)常會(huì)看到這么一道題:

  var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());//result:The Window

《javascript高級(jí)程序設(shè)計(jì)》一書(shū)給出的解釋是:

this對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中,this等于window,而當(dāng)函數(shù)被作為某個(gè)對(duì)象調(diào)用時(shí),this等于那個(gè)對(duì)象。不過(guò),匿名函數(shù)具有全局性,因此this對(duì)象同常指向window


作者:mayday526
來(lái)源:CSDN
原文:https://blog.csdn.net/whd526/article/details/70990994
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請(qǐng)附上博文鏈接!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容