JavaScript:做用域與做用域鏈

1.什麼是做用域(scope)?

簡單來說,做用域(scope)就是變量訪問規則的有效範圍javascript

  • 做用域外,沒法引用做用域內的變量;
  • 離開做用域後,做用域的變量的內存空間會被清除,好比執行完函數或者關閉瀏覽器
  • 做用域與執行上下文是徹底不一樣的兩個概念。我曾經也混淆過他們,可是必定要仔細區分。

JavaScript代碼的整個執行過程,分爲兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段做用域規則會肯定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段建立。java

函數做用域是在函數聲明的時候就已經肯定了,而函數執行上下文是在函數調用時建立的。假如一個函數被調用屢次,那麼它就會建立多個函數執行上下文,可是函數做用域顯然不會跟着函數被調用的次數而發生什麼變化。jquery

1.1 全局做用域

var foo = 'foo';
console.log(window.foo);   // => 'foo' 

在瀏覽器環境中聲明變量,該變量會默認成爲window對象下的屬性。瀏覽器

function foo() {
    name = "bar"
}
foo();
console.log(window.name) // bar

在函數中,若是不加 var 聲明一個變量,那麼這個變量會默認被聲明爲全局變量,若是是嚴格模式,則會報錯。閉包

全局變量會形成命名污染,若是在多處對同一個全局變量進行操做,那麼久會覆蓋全局變量的定義。同時全局變量數量過多,很是不方便管理。函數

這也是爲何jquery要在全局創建變量 ,其他私有方法屬性掛在 下的緣由。post

1.2 函數做用域

假如在函數中定義一個局部變量,那麼該變量只能夠在該函數做用域中被訪問。atom

function doSomething () {
    var thing = '吃早餐';
}
console.log(thing); // Uncaught ReferenceError: thing is not defined

嵌套函數做用域:spa

function outer () {
    var thing = '吃早餐';
    function inner () {
        console.log(thing);
    }
    inner();
}

outer();  // 吃早餐

 

在外層函數中,嵌套一個內層函數,那麼這個內層函數能夠向上訪問到外層函數中的變量。翻譯

既然內層函數能夠訪問到外層函數的變量,那若是把內層函數return出來會怎樣?

function outer () {
    var thing = '吃早餐';
    
    function inner () {
        console.log(thing);
    }
    
    return inner;
}

var foo = outer();
foo();  // 吃早餐

函數執行完後,函數做用域的變量就會被垃圾回收。而這段代碼看出當返回了一個訪問了外部函數變量的內部函數,最後外部函數的變量得以保存。

這種當變量存在的函數已經執行結束,但扔能夠再次被訪問到的方式就是「閉包」。後期會繼續對閉包進行梳理。

1.3 塊級做用域

不少書上都有一句話,javascript沒有塊級做用域的概念。所謂塊級做用域,就是{}包裹的區域。可是在ES6出來之後,這句話並不那麼正確了。由於能夠用 let 或者 const 聲明一個塊級做用域的變量或常量。

好比:

for (let i = 0; i < 10; i++) {
    // ...
}
console.log(i); // Uncaught ReferenceError: i is not defined

發現這個例子就會和函數做用域中的第一個例子同樣的錯誤提示。由於變量i只能夠在 for循環的{ }塊級做用域中被訪問了。

擴散思考:

究竟何時該用let?何時該用const?

默認使用 const,只有當確實須要改變變量的值的時候才使用let。由於大部分的變量的值在初始化以後不該再改變,而預料以外的變量的修改是不少bug的源頭。

1.4 詞法做用域

詞法做用域,也能夠叫作靜態做用域。意思是不管函數在哪裏調用,詞法做用域都只在由函數被聲明時所處的位置決定。
既然有靜態做用域,那麼也有動態做用域。
而動態做用域的做用域則是由函數被調用時執行的位置所決定。

var a = 123;
function fn1 () {
    console.log(a);
}
function fn2 () {
    var a = 456;
    fn1();
}
fn2();   // 123

以上代碼,最後輸出結果 a 的值,來自於 fn1 聲明時所在位置訪問到的 a 值 123。
因此JS的做用域是靜態做用域,也叫詞法做用域。

上面的1.1-1.3能夠看作做用域的類型。而這一小節,其實跟上面三小節仍是有差異的,並不屬於做用域的類型,只是關於做用域的一個補充說明吧。

2. 什麼是做用域鏈(scope chain)

在JS引擎中,經過標識符查找標識符的值,會從當前做用域向上查找,直到做用域找到第一個匹配的標識符位置。就是JS的做用域鏈。

var a = 1;
function fn1 () {
    var a = 2;
    function fn2 () {
        var a = 3;
        console.log(a);
    }
    fn2 ();
}
fn1(); // 3

console.log(a) 語句中,JS在查找 a變量標識符的值的時候,會從 fn2 內部向外部函數查找變量聲明,它發現fn2內部就已經有了a變量,那麼它就不會繼續查找了。那麼最終結果也就會打印3了。

代碼分析以下:

<script type="text/javascript">
    var a = 100;
    function fun(){
        var b = 200
        function fun2(){
            var c = 300
        }
        function fun3(){
            var d = 400
        }
        fun2()
        fun3()
    }
    fun()
</script>

首先預編譯,一開始生成一個GO{

  a:underfined

  fun:function fun(){//fun的函數體

      var b = 200
      function fun2(){
        var c = 300
      }
      function fun3(){
      var d = 400
      }
      fun2()
      fun3()
    }

}

逐行執行代碼,GO{

  a:100

  fun:function fun(){//fun的函數體

      var b = 200
      function fun2(){
        var c = 300
      }
      function fun3(){
      var d = 400
      }
      fun2()
      fun3()
    }

}

當fun函數執行時,首先預編譯會產生一個AO{

  b:underfined

  fun2:function fun2(){
       var c = 300
     }

  fun3:function fun3(){
      var d = 400
     }

}

這裏注意的是fun函數是在全局的環境下產生的,因此本身身上掛載這一個GO,因爲做用域鏈是棧式結構,先產生的先進去,最後出來,

在這個例子的狀況下,AO是後於GO產生的,因此對於fun函數自己來講,執行代碼的時候,會先去本身自己的AO裏找找看,若是沒有找到要用的東西,就去父級查找,此題的父級是GO

此刻fun的做用域鏈是  第0位    fun的AO{}

          第1位    GO{}

fun函數開始逐行執行AO{

  b:200

  fun2:function fun2(){
       var c = 300
     }

  fun3:function fun3(){
      var d = 400
     }

 }

注意:函數每次調用纔會產生AO,每次產生的AO還都是不同的

而後遇到fun2函數的執行,預編譯產生本身的AO{

  c:underfined

}

此刻fun2的做用域鏈是第0位    fun2的AO{}

          第1位    fun的AO{}

          第2位    GO{}

而後遇到fun3函數的執行,預編譯產生本身的AO{

  d:underfined

}

此刻fun3的做用域鏈是第0位    fun3的AO{}

          第1位    fun的AO{}

          第2位    GO{}

fun2和fun3的做用域鏈沒有什麼聯繫。

當函數fun2和fun3執行完畢,本身將砍掉本身和本身的AO的聯繫,

最後就是fun函數執行完畢,它也是砍掉本身和本身AO的聯繫。

這就是一個咱們平時看到不是閉包的函數。

 

閉包

1.閉包在紅寶書中的解釋就是:有權訪問另外一個函數做用域中的變量的函數。

2.寫法:

 1 <script type="text/javascript">
 2     function fun1(){
 3         var a = 100;
 4         function fun2(){
 5             a++;
 6             console.log(a);
 7         }
 8         return fun2;
 9     }
10     
11     var fun = fun1();
12     fun()
13     fun()
14 </script>

3.效果以下:

4.分析:

執行代碼

GO{

fun:underfined

fun1:function fun1()

   {

     var a = 100;

     function fun2()

    {

        a++;

        console.log(a);

     }

     return fun2;

     }

 

}

而後第十一行開始這裏,就是fun1函數執行,而後把fun1的return返回值賦給fun,這裏比較複雜,咱們分開來看,

這裏fun1函數執行,產生AO{

a:100

fun2:function fun2(){

    a++;
    console.log(a);
    }

}

此刻fun1的做用域鏈爲 第0位   AO

           第1位   GO

此刻fun2的做用域鏈爲 第0位   fun1的AO

           第1位   GO

解釋一下,fun2只是聲明瞭,並無產生調用,因此沒有產生本身的AO,

正常的,咱們到第7行代碼咱們就結束了,可是這個時候來了一個return fun2,把fun2這個函數體拋給了全局變量fun,好了,fun1函數執行完畢,消除本身的AO,

此刻fun2的做用域鏈爲 第0位   fun1的AO

           第1位   GO

第十二行就是fun執行,而後,它自己是沒有a的,可是它能夠用fun1的AO,而後加,而後打印,

由於fun中的fun1的AO原本是應該在fun1銷燬時,去掉,可是被拋給fun,因此如今fun1的AO沒辦法銷燬,因此如今a變量至關於一個只能被fun訪問的全局變量。

因此第十三行再調用一次fun函數,a被打印的值爲102。

相關文章
相關標籤/搜索