动态

详情 返回 返回

深入理解JavaScript之執行上下文、閉包 - 动态 详情

在理解閉包之前,需要先來了解幾個概念,上下文、作用域鏈、活動對象、變量對象:

  • 上下文:函數的上下文決定了他們可以訪問哪些數據,以及他們的行為。全局上下文是最外層的上下文,當代碼執行流進入到函數時,函數的上下文被推到上下文棧上,當函數執行完之後,上下文棧會彈出該函數上下文。
  • 作用域鏈:上下文中代碼執行的時候會創建作用域鏈,它決定了各級上下文中代碼訪問變量或函數的順序。代碼正在執行的上下文變量對象總是位於作用域鏈最頂端,然後是包含上下文對象,然後是下一個包含上下文對象,直至到全局上下文對象為止。
  • 活動對象:函數上下文中,包含其中變量的對象。
  • 變量對象:全局上下文中,包含其中變量的對象。

我們來總結一下,在調用一個函數時,它的作用域鏈都保存了哪些對象?

  1. 在函數被調用時,先創建一個執行上下文,並創建一個作用域鏈。
  2. argument和其他命名參數初始化該函數的活動對象。
  3. 外部函數的活動對象是該函數作用域鏈的第二個對象,該作用域鏈一直向外部串起所有的包含該函數的活動對象,知道全局上下文才終止。

下面聲明並調用了一個方法compare(),現在來梳理一下該方法從創建到執行的過程:

  function compare(value1, value2) {
    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  }

  let result = compare(5, 6);

1.執行全局代碼,創建全局執行上下文,全局上下文被壓入執行上下文棧

    ECStack = [//執行上下文棧
        globalContext
    ];

2.全局上下文變量即變量對象初始化,初始化的同時,compare 函數被創建,創建作用域鏈,並在內部屬性[[scope]]中預裝載全局變量對象。

  globalContext = {//變量對象初始化
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }

  compare.[[scope]] = [//compare在作用域鏈預裝載全局變量對象
        globalContext.VO
  ];

3.執行 compare 函數,創建 compare 函數執行上下文,compare 函數執行上下文被壓入執行上下文棧

  ECStack = [//執行上下文棧
        comapreContext,
        globalContext
    ];

4.comapre 函數執行上下文初始化併為變量賦值:
1)複製函數 [[scope]] 屬性創建作用域鏈,
2)用 arguments 創建活動對象,
3)初始化活動對象,即加入形參、函數聲明、變量聲明,
4)將活動對象壓入 comapre 作用域鏈頂端。

    comapreContext = {
        AO: {
            arguments: {
                0:5
                1:6
                length: 2
            },
            scope: undefined,
        },
        Scope: [AO, globalContext.VO],
    }

5.執行代碼,函數執行完畢後返回,並將函數comapre的執行上下文從執行上下文棧中彈出。

    ECStack = [
        globalContext
    ];

compare()方法的作用域鏈如下圖:
作用域鏈實際上是一個包含指針的列表,每個指針分別指向一個變量對象,但是物理上不會包含相應的對象。

image.png

閉包

《JavaScript高級編程》: 閉包是指那些引用了另一個函數作用域中變量的函數,通常是在嵌套函數中實現的。
MDN--閉包:一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者説函數被引用包圍),這樣的組合就是閉包(closure)。

定義略顯抽象,借用阮一峯老師的理解:閉包就是能夠讀取其他函數內部變量的函數。 由於在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成"定義在一個函數內部的函數"。所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。

  var winVar = 'window-小白';

  function fun() {
    var funVar = 'fun-小白';
    console.log(winVar)//window-小白
    console.log(funVar)//fun-小白
  }

  fun();
  console.log(funVar)//Uncaught ReferenceError: funVar is not defined

函數作用域鏈如下圖:當函數在作用域鏈頂端找不到winVar對象,就會去全局變量對象中尋找,而在winodow中,無法訪問到fun()函數的變量,因此需要使用閉包。
image.png
閉包可以用在許多地方。它的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量的值始終保持在內存中

  function fun() {
    var funVar = 'fun-小白';
    return function () {
      return funVar;
    }
  }

  let variable = fun();
  let result = variable();
  console.log(result)//fun-小白

函數的作用域鏈如下圖,在fun方法返回匿名函數後,匿名函數的作用域鏈被初始化為包含fun的活動對象和全局變量對象,雖然fun()函數執行結束後,其執行上下文的作用域鏈會銷燬,但是在匿名函數的作用域鏈中,仍然有對他的引用。這樣應該就不難理解為什麼閉包可以讀取函數內部的變量,也可以讓變量的值始終保持在內存中了。
image.png

返回的匿名函數被保存在variable方法中,把variable()方法設置為null可以解除對函數的引用,從而讓垃圾回收程序將內存釋放掉。 variable = null;

同時因為閉包會保留包含它們的函數作用域,所以比其他函數更佔內存,過度使用閉包會導致內存過度佔用,所以不能濫用閉包,否則會造成網頁的性能問題。

參考資料:

  • MDN--閉包
  • 阮一峯的網絡日誌
  • JavaScript深入之作用域鏈
  • JavaScript深入之閉包
  • 《JavaScript高級編程(第四版)》
user avatar toopoo 头像 linlinma 头像 hard_heart_603dd717240e2 头像 u_17443142 头像 xiaolei_599661330c0cb 头像 yqyx36 头像 assassin 头像 user_ze46ouik 头像 yanyue404 头像 heath_learning 头像 tizuqiudehongcha 头像 morimanong 头像
点赞 46 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.