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

作用域

先來談?wù)勛兞康淖饔糜?/h5>
  • 變量的作用域無非就是兩種:全局變量和局部變量
  • 全局作用域:最外層函數(shù)定義的變量擁有全局作用域,即對任何內(nèi)部函數(shù)來說,都是可以訪問的,看例子:
var outVar = "全局變量";
function fn() {
    console.log(outVar);
}
fn();  //"全局變量"
  • 局部作用域:和全局作用域相反,一般只在固定的代碼片段內(nèi)可以訪問到,而對于函數(shù)外部是無法訪問的,最常見的例如函數(shù)內(nèi)部,看例子:
function fn(){
    var innerVar = "inner";
 }
    fn();
   console.log(innerVar);// ReferenceError: innerVar is not defined

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

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

再來看一端代碼:

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

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

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

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

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

但是javascript不同,并沒有所謂的塊級作用域,javascript的作用域是相對函數(shù)而言的,可以稱為函數(shù)作用域:

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


作用域鏈(Scope Chain)

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

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

  1. 是js中最為重要的概念,執(zhí)行環(huán)境定義了變量和函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了它們各自的行為。
  2. 執(zhí)行環(huán)境,總共有三種,全局環(huán)境,和局部環(huán)境(函數(shù)),eval

變量對象:

  1. 每個執(zhí)行環(huán)境,都有一個與之關(guān)聯(lián)的變量對象,執(zhí)行環(huán)境中定義的所有的變量和函數(shù)都保存在這個對象中
  2. 雖然我們編寫的代碼無法訪問這個對象,但是解析器在處理數(shù)據(jù)的時候會在回臺使用它。

全局執(zhí)行環(huán)境:

  1. 是最外圍的一個執(zhí)行環(huán)境,根據(jù)ECMAScript 實現(xiàn)的所在宿主環(huán)境不同,表示執(zhí)行環(huán)境的對象也不一樣
  2. 在 Web瀏覽器中,全局執(zhí)行環(huán)境,被認為是window對象,因此所有的全局變量和函數(shù),都是作為window對象的屬性和方法創(chuàng)建的;
  3. 某個執(zhí)行環(huán)境中的所有代碼執(zhí)行完畢后,該環(huán)境銷毀,保存在其中所有變量和函數(shù)定義也會銷毀
  4. 全局執(zhí)行環(huán)境直到應用退出(例如關(guān)閉網(wǎng)頁,或者瀏覽器時)才會被銷毀

環(huán)境棧:

  1. 每個函數(shù),都有自己的執(zhí)行環(huán)境,當執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境就會被推入一個環(huán)境棧中,而在函數(shù)執(zhí)行之后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境,ECMAScript 程序中的執(zhí)行流正是由這個方便的機制控著

作用域鏈:

  1. 作用域鏈本質(zhì)上,是一個指向變量對象的指針列表,它只引用,但不實際包含變量對象
  2. 當代碼在一個環(huán)境中執(zhí)行時,會創(chuàng)建變量對象的一個作用域鏈,作用域鏈的用途,是保證對執(zhí)行環(huán)境有權(quán)訪問的變量和函數(shù)的有序訪問,作用域鏈的前端,始終都是當前執(zhí)行的代碼所在環(huán)境的變量對象
  3. 如果這個環(huán)境是函數(shù),則其活動對象作為變量對象,活動對象,在最開始時只包含一個變量即argumengts對象(這個對象,在全局環(huán)境中是不存在的)作用域鏈中的下一個變量對象來自包含(外部)環(huán)境,而再下一個變量對象則來自于下一個包含環(huán)境,這樣,一直延伸到全局執(zhí)行環(huán)境;
    4.全局執(zhí)行環(huán)境的變量對象,始終都是作用域鏈中的最后一個對象

標識符的解析:

  1. 是沿著作用域鏈一級一級的搜索標識符的過程,搜索過程始終從作用域鏈的前端開始,然后逐級地向后回溯,直至找到標識符為止(如果找不到標識符,通常會導致錯誤發(fā)生)
注意:內(nèi)部環(huán)境,可以通過作用域鏈訪問所有的外部環(huán)境,但是外部環(huán)境不能訪問內(nèi)部環(huán)境中的任何變量和函數(shù),這些環(huán)境之間的 聯(lián)系,是線性,有次序的,每個環(huán)境都可以向上搜索作用域鏈,以查詢變量和函數(shù)名,但是任何環(huán)境都不能通過向下搜索作用域鏈而進入另一個執(zhí)行環(huán)境

舉個例子:

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

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


image.png

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


image.png

可以看到fn1活動對象里并沒有scope變量,于是沿著作用域鏈(scope chain)向后尋找,結(jié)果在全局變量對象里找到了scope,所以就返回全局變量對象里的scope值。
那作用域鏈地作用僅僅只是為了搜索標識符嗎?

再來看一段代碼:

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

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


image.png

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


image.png

像上面這種內(nèi)部函數(shù)的作用域鏈仍然保持著對父函數(shù)活動對象的引用,就是閉包(closure)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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