從變量提高、執行上下文到閉包、this

1、變量提高

  1. 什麼是變量提高?數組

    變量提高是指在 JS 代碼的執行過程當中,JavaScript 引擎把變量和函數的聲明部分提高到代碼開頭的行爲。瀏覽器

  2. 爲何會有變量提高?閉包

    ES6 以前的 JS 沒有塊級做用域,因此把做用域內部的變量統一提高是最快速、最簡單的設計。app

  3. 變量提高帶來的問題函數

    (1) 變量容易在不被察覺的狀況下被覆蓋掉;工具

    (2) 本應銷燬的變量沒有被銷燬。性能

  4. 同名變量和函數的兩點處理原則優化

    (1) 若是是同名的函數,JavaScript 編譯階段會選擇最後聲明的那個;ui

    (2) 若是變量和函數同名,那麼在編譯階段,變量的聲明會被忽略。執行階段變量正常賦值。this

    console.log(foo)     // 執行結果爲:f foo() {},說明變量foo的聲明被忽略了
    function foo() {}
    var foo = 2;
    console.log(foo)     // 執行結果爲:2,說明變量foo的賦值被執行了
    複製代碼
  5. 如何解決變量提高帶來的問題

    ES6 引入let,讓 JavaScript 也有塊級做用域。

  6. 在同一段代碼中,ES6 是如何作到既支持變量提高又支持塊級做用域的?

    JS 代碼在執行過程當中會經歷編譯和執行兩個過程。

    • 編譯階段,建立執行上下文時,經過var聲明的變量,被放到執行上下文的變量環境中,經過let聲明的變量,被放到執行上下文的詞法環境中。塊級做用域中經過let聲明的變量沒有被放到詞法環境。
    • 執行階段,進入塊級做用域時,經過let聲明的變量,被存放到當前執行上下文的詞法環境中一個單獨的區域,這個區域並不影響塊級做用域外面的變量,即這個區域和區域外的同名變量是獨立的存在。
  7. 什麼是變量環境和詞法環境?

    變量環境和詞法環境都是執行上下文中定義的對象。變量環境對象保存的是變量提高的內容,詞法環境對象保存的是經過letconst聲明的變量。

2、執行上下文

  1. 什麼是執行上下文?

    JS 代碼在編譯的時候會生成執行上下文和可執行代碼。執行上下文是 JavaScript 執行一段代碼時的環境,其中存在一個變量環境的對象,該對象保存了變量和函數的聲明部分。

  2. 什麼狀況下會建立執行上下文?

    • JavaScript 執行全局代碼的時候,會編譯全局代碼並建立全局執行上下文,並且在整個頁面的生存週期內,全局執行上下文只有一份。
    • 當調用一個函數的時候,函數體內的代碼會被編譯,並建立函數執行上下文,通常狀況下,函數執行結束後,建立的執行上下文會被銷燬。
    • 當使用eval函數的時候,eval的代碼也會被編譯,並建立執行上下文。
  3. 如何管理執行上下文?

    JavaScript 用調用棧管理上下文。

3、調用棧

  1. 什麼是調用棧?

    調用棧是管理執行上下文的棧,是 JavaScript 引擎追蹤函數執行的一個機制,經過調用棧能夠追蹤到哪一個函數正在被執行以及各函數之間的調用關係。

  2. 代碼執行過程當中,調用棧是如何變化的?

    (1) 首先建立全局上下文,並將其壓入棧底,而後執行全局上下文。

    (2) 執行到函數調用代碼時,編譯被調用的函數,爲其建立一個執行上下文,並將執行上下文壓入棧中。而後執行函數代碼。

    (3) 再次執行到函數調用代碼時,重複第 (2) 步。

    (4) 棧頂函數執行完後,其執行上下文從棧頂彈出,並將函數的返回值賦給調用函數的相應變量。

    (5) 繼續執行棧頂函數,並重復第 (4) 步,直至全局代碼執行完畢。

  3. 如何在瀏覽器中查看調用棧的信息?

    以 Chrome 瀏覽器爲例,在開發者工具的"Sources"工具欄中,給指定代碼加上斷點,代碼執行到斷點處時暫停執行,在右側的"Call Stack"中查看當前的調用棧。

    另外一種方法是,在函數代碼中加上console.trace()語句,而後在控制檯查看輸出結果。

  4. 什麼是棧溢出?什麼狀況下會出現棧溢出?

    調用棧是有大小的,當入棧的執行上下文超過必定數目時,JavaScript 引擎就會報棧溢出的錯誤:

    Uncaught RangeError: Maximum call stack size exceeded
    複製代碼

    沒有終止條件的遞歸函數,會一直建立新的執行上下文,並反覆將執行上下文壓入調用棧中,最終超出棧的容量限制,致使棧溢出。

4、做用域

  1. 什麼是做用域?

    做用域是指在程序中定義變量的區域,該位置決定了變量的生命週期。通俗地理解,做用域就是變量與函數的可訪問範圍,即做用域控制着變量和函數的可見性和生命週期。

  2. 有哪幾種做用域?

    ES6 以前,只有兩種做用域:全局做用域和函數做用域。ES6 增長了塊級做用域。

    (1) 全局做用域中的對象在代碼中的任何地方都能被訪問,其生命週期伴隨着頁面的生命週期。

    (2) 函數做用域就是在函數內部定義的變量或函數,而且定義的變量或者函數只能在函數內部被訪問。函數執行結束以後,函數內部定義的變量會被銷燬。

    (3) 塊級做用域是在大括號內代碼塊中使用letconst聲明的變量,變量只能在代碼塊內被訪問。代碼塊執行結束後,代碼塊內定義的變量會被銷燬。

  3. 什麼是做用域鏈?

    每一個執行上下文的變量環境中,都包含了一個外部引用,用來指向外部的執行上下文。當一段代碼使用了一個變量時,JavaScript 引擎首先會在當前的執行上下文中查找該變量,若是沒有找到,就繼續在外部引用所指向的執行上下文中查找。這個查找的鏈條就是做用域。

  4. 變量的具體查找方式?

    在當前執行上下文中,沿着詞法環境的棧頂向下查詢,若是找到了變量,就直接返回給 JavaScript 引擎,若是沒有找到,就繼續在變量環境中查找,找到了就返回,沒找到就在當前執行上下文的外部引用所指向的執行上下文中查找。

  5. 爲何函數的執行上下文的外部引用不是調用它的函數的執行上下文?

    由於在 JavaScript 執行過程當中,其做用域鏈是由詞法做用域決定的。詞法做用域是指做用域是由代碼中函數聲明的位置來決定的,因此詞法做用域是靜態的做用域,在代碼階段就決定好了,和函數是怎麼調用的沒有關係。經過詞法做用域可以預測代碼在執行過程當中如何查找標識符。

5、閉包

  1. 什麼是閉包?

    在 JavaScript 中,根據詞法做用域的規則,內部函數老是能夠訪問其外部函數中聲明的變量,當經過調用一個外部函數返回一個內部函數後,即便外部函數已經執行結束了,內部函數引用外部函數的變量也依然保存在內存中,這些變量的集合稱爲閉包

  2. 如何在瀏覽器中查看閉包?

    打開 Chrome 的開發者工具,在使用閉包的函數的任意地方打上斷點,而後刷新頁面,代碼執行到斷點處時,能夠在右側"Scope"中的"Closure"查看閉包。

  3. 閉包中變量的查找方式?

    先在當前執行上下文中查找,若是沒有找到,就查找外部函數的閉包,仍沒有找到就去外部函數的外部引用所指向的執行上下文中查找。體如今"Scope"中,這個查找過程就是:Local -> Closure -> Global

  4. 閉包是怎麼回收的?

    一般,若是引用閉包的函數是一個全局變量,那麼閉包會一直存在直到頁面關閉;但若是這個閉包之後再也不使用的話,就會形成內存泄漏。若是引用閉包的函數是個局部變量,等函數銷燬後,在下次 JavaScript 引擎執行垃圾回收時,判斷閉包這塊內容若是已經再也不被使用了,垃圾回收器就會回收這塊內存。

  5. 閉包的應用場景

    一般使用只有一個方法的對象的地方,均可以使用閉包。

    (1) 爲響應事件而執行的函數。

    function makeSizer(size) {
        return function() {
            document.body.style.fontSize = size + 'px';
        };
    }
    document.getElementById('size-12').onclick = makeSizer(12);
    document.getElementById('size-14').onclick = makeSizer(14);
    複製代碼

    (2) 用閉包模擬私有方法:用閉包定義公共函數,並令其能夠訪問私有函數和變量。這個方式稱爲「模塊模式」。

    var makeCounter = function() {
        var privateCounter = 0;
        function changeBy(val) {
            privateCounter += val;
        }
        return {
            increment: function() {
                changeBy(1);
            },
            document: function() {
                changeBy(-1);
            },
            value: function() {
                return privateCounter;
            }
        }
    }
    
    var Counter1 = makeCounter();
    // Counter1和Counter2指向兩個不一樣的對象,在一個閉包內對變量的修改,不會影響到另一個閉包中的變量
    var Counter2 = makeCounter();
    var Counter3 = Counter1;       // Counter3和Counter1指向同一個對象,會相互影響
    複製代碼

    (3) 解決循環中使用var定義變量形成的常見錯誤。

    var a = [];
    
    for (var i = 0; i < 10; i++) {
        a[i] = {};
        a[i].log = function() {
            console.log(i);
        }
    }
    
    a[2].log();    // 10
    複製代碼

    問題:調用數組a任何一個元素的log方法,都打印10。

    緣由:數組a中每一個元素中的log屬性的值都爲function() { console.log(i) },for循環執行結束後,i沒有被銷燬,值爲10,因此打印的結果都是10。

    解決方案:

    1. 增長一個閉包

      var a = [];
      
      function set(x) {
          return function() {
              console.log(x);
          }
      }
      
      for (var i = 0; i < 10; i++) {
          a[i] = {};
          a[i].log = set(i);
      }
      
      a[2].log()   // 2
      複製代碼

      原理:能夠理解爲閉包幫忙保存了每次執行循環時當前的i值。

      • 數組a中第i個元素的log屬性值爲set(i)函數的返回值;
      • set函數編譯時,變量x進入set函數的執行上下文,執行時,x被賦值爲傳入的i
      • set函數執行完後,其執行上下文從調用棧頂部彈出,但閉包中的變量x依然保存在內存中,a[i]被賦值爲set函數返回的函數function() { console.log(x) }
      • 數組中每一個元素的log屬性被賦值時,都會從新編譯並執行set函數,生成新的函數執行上下文,函數執行完後,其執行上下文被銷燬,但被賦值爲當前i值的x都繼續保存在內存中,因此每一個元素的log方法所」擁有「的x值都不同。
    2. 使用當即執行的匿名函數

      var a = [];
      
      for (var i = 0; i < 10; i++) {
          a[i] = {};
          a[i].log = function(num) {
              return function() {
                  console.log(num);
              }
          }(i)
      }
      
      a[2].log()  // 2
      複製代碼

      原理:和第一種方案相同。

    3. 使用let關鍵字

      var a = [];
      
      for (let i = 0; i < 10; i++) {
          a[i] = {};
          a[i].log = function() {
              console.log(i);
          }
      }
      
      a[2].log()  // 2
      複製代碼

      原理:花括號加let關鍵字造成了塊級做用域。數組a的每一個元素的log方法的[[Scopes]]下都有一個Block屬性保存塊級做用域的值,以下圖所示。

      avatar

      使用var關鍵字的結果以下圖所示。

      avatar

  6. 合理使用閉包

    若是不是某些特定任務須要使用閉包,不建議在其餘函數中建立函數,由於閉包在處理速度和內存消耗方面對腳本性能具備負面影響。

    例如,在建立新的對象或者類型時,方法一般應該關聯於對象的原型,而不是定義到對象的構造函數中。緣由是後者會致使每次構造函數被調用時,方法都會被從新賦值一次。

6、this指向

  1. JavaScript 中爲何會出現this

    JavaScript 的做用域機制不支持在對象內部的方法中使用對象內部的屬性,因此使用this來解決這個問題。

  2. this是什麼?

    this是和執行上下文綁定的,每一個執行上下文都有一個this

    結合上文所述,執行上下文中包含:變量環境、詞法環境、outer(當前執行上下文指向外部執行上下文的引用)、this

  3. 不一樣執行上下文中的this

    (1) 全局上下文中的this:指向window對象。這也是this和做用域鏈的惟一交點,做用域鏈的最低端包含了window對象,全局執行上下文中的this也是指向window對象。

    (2) 函數執行上下文中的this

    • 經過函數的call方法設置的this:調用call方法的函數中的this指向了call方法傳入的第一個參數。也能夠經過applybind方法設置函數執行上下文的this

      let bar = {
          myName: 'Helena'
      }
      function foo() {
          this.myName = 'Wucan'
      }
      foo.call(bar)
      console.log(bar.myName)  // Wucan
      複製代碼
    • 經過對象調用方法設置this:使用對象來調用其內部的一個方法,該方法的this是指向對象自己的。

      var myObj = {
          name: 'Helena',
          showThis: function() {
              console.log(this.name);
          }
      }
      myObj.showThis()  // Helena
      myObj.showThis.call(myObj)   // Helena
      
      var foo = myObj.showThis;
      foo()   // undefined,this指向了全局對象
      複製代碼

      結論:

      • 在全局環境中調用一個函數,函數內部的this指向的是全局變量window
      • 經過一個對象來調用其內部的一個方法,該方法的執行上下文中的this指向對象自己
    • 在構造函數中設置this

      function Person() {
          this.name = 'name'
      }
      var p = new Person()
      複製代碼

      經過new和構造函數建立的對象中的this指向對象自己。

  4. this的設計缺陷及應對方案?

    (1) 嵌套函數中的this不會從外層函數中繼承

    var name = 'Wucan';
    
    var myObj = {
        name: 'Helena',
        showThis: function() {
            console.log(this.name);     // Helena,this指向myObj
            function bar() {
                console.log(this.name);    // Wucan,this指向window
            }
            bar();
        }
    }
    
    myObj.showThis()
    複製代碼

    解決方案:

    • 第一種方案:把this保存爲一個變量,再利用變量的做用域機制傳遞給嵌套函數。
    • 第二種方案:把嵌套函數改成箭頭函數,由於箭頭函數沒有本身的執行上下文,因此它會繼承調用函數中的this

    (2) 普通函數中的this默認指向全局對象window

    • 第一種方案:經過調用call方法改變this的指向。
    • 第二種方案:設置 JavaScript 爲嚴格模式。嚴格模式下,默認執行一個函數,其函數執行上下文中的this值是undefined

問題:

  1. 下面這段代碼會產生棧溢出的問題,如何優化它,以解決棧溢出的問題?

    function runStack (n) {
      if (n === 0) return 100;
      return runStack( n- 2);
    }
    runStack(50000)
    複製代碼

    試着改成如下代碼便可不溢出:

    function rs(n) {
        while (n !== 0)
            n = n - 2;
        n = 100;
        return n;
    }
    rs(58579862)
    複製代碼

    想試一下傳入多大的參數會出現異常,因而試了下:這個值的臨界點有時是58579862,有時是58999998,反正不是個肯定的值。想知道這個值的大小和什麼有關係?爲何超過了臨界值就不會輸出結果,也不報錯?

  2. 下面這段代碼,想結合上述概念手動模擬一下編譯和執行的整個過程:

    function foo() {
        var myName = "極客時間"
        let test1 = 1
        const test2 = 2
        var innerBar = {
            getName:function(){
                console.log(test1)
                return myName
            },
            setName:function(newName){
                myName = newName
            }
        }
        return innerBar
    }
    var bar = foo()
    bar.setName("極客邦")
    bar.getName()
    console.log(bar.getName())
    複製代碼

    打斷點查看的時候,發現有一個地方與想象中不同。代碼執行到斷點處時,按照先編譯再執行的思路,Local中應該有四個值爲undefined的變量:myNametest1test2innerBar,實際卻以下圖所示。test1是在執行完let test1 = 1後出如今Local中且同時被賦值爲2的。

    avatar

    覺得是letconst聲明在編譯時仍是有區別的,因而嘗試作如下兩種修改:

    (1) let test1 = 1 改成const test1 = 1,結果同樣;

    (2) 將getName()方法中的console.log(test1)改成console.log(test2),斷點執行以下圖,test2變成了test1

    avatar

    因此猜測這裏是否與閉包中引用了外部函數的變量有關?


參考:

  1. 【極客時間】瀏覽器工做原理,李兵。
  2. 【MDN】閉包:developer.mozilla.org/zh-CN/docs/…
相關文章
相關標籤/搜索