動態

詳情 返回 返回

深入理解JavaScript閉包 - 動態 詳情

在開始講閉包之前,我們需要理解作用域和作用域鏈

作用域鏈

什麼是作用域鏈?

我們先看一段代碼

function bar(){
    console.log(myName)
}
function foo(){
    var myName='崔斯特'
    bar()
}
var myName='卡牌大師'
foo()

當我們看到這個題目的時候,我們會想到用執行上下文去分析,當執行到bar函數時,調用棧的狀態如圖:

上圖可以看到有兩個myName變量,那bar執行的時候用的是哪一個呢?

其實在每個執行上下文的環境變量中,都包含了一個外部引用(稱為outerouterouter),指向外部的執行上下文。

當在bar函數的執行上下文中沒有找到myName變量的時候,會通過outer去外部的執行上下文中找這個變量。而bar的outer是直接指向全局執行上下文,然後在全局執行上下中,先在詞法環境中從棧頂到棧底查找,如果沒有再到變量環境中查找。

有人可能會問:為什麼是foo函數調用bar函數,但是outer指向的是全局上下文?

  • 這個其實和詞法作用域(靜態作用域)有關,簡單來説就是代碼結構中函數聲明的位置來決定,上述的代碼中,foo函數和bar函數的上一級作用域是全局作用域,所以如果foo或bar數調用了一個它們沒有定義的變量,它們就會到上一級作用域中查找。説白了:詞法作用域是代碼階段就決定好了,和函數是怎麼調用沒有關係

到這裏我們已經説明了什麼是作用域鏈了:js沿着詞法作用域形成鏈條一層層往外查找,這個查找的鏈條就叫做作用域鏈

塊級作用域的查找

我們來説説上述的查找過程,當執行到bar函數的if語句時,因為bar函數的執行上下文中沒有定義test變量,根據詞法作用域規則,就會到bar函數的外部作用域中查找,也就是全局作用域。在單個執行上下文中的查找規則:先在詞法環境中從棧頂到棧底查找,如果沒有再到變量環境中查找。

閉包

瞭解完作用域鏈之後,我們從作用域鏈的角度來講講什麼是閉包!

先看一段代碼

function foo(){
    var myName = "崔斯特"
    let test1 = 1
    const test2 = 2
    var innerBar={
        setName:function (newName){
            myName = newName
        },
        getName:function (){
            console.log(test1)
            return myName
        }

    }
    return innerBar
}
var bar = foo()
bar.setName("卡牌大師")
bar.getName()
console.log(bar.getName())

根據詞法作用域原則,innerBar中的兩個方法可以訪問foo函數的兩個變量,當inner函數被返回給全局bar變量時,雖然foo函數已經執行結束,但是getName和setName函數依然可以使⽤foo函數中的變量myName和test1。

看到這裏我們可以給閉包下一個定義了!在JavaScript中,根據詞法作⽤域的規則,內部函數總是可以訪問其外部函數中聲明的變量,當通過調⽤⼀個外部函數返回⼀個內部函數後,即使該外部函數已經執⾏結束了,但是內部函數引⽤外部函數的變量依然保存在內存中,我們就把這些變量的集合稱為閉包。⽐如外部函數是foo,那麼這些變量的集合就稱為foo函數的閉包。

閉包的回收

當我們閉包使用不正確時,很容易造成內存泄漏

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

接下來我們從內存模型來深入理解一下閉包!

我們先來了解一下js在運行的過程中,數據是怎麼存儲的

在js的執行中有三種內存空間:代碼空間、棧空間、堆空間

代碼空間是存儲可執行代碼的,我們主要來看看棧空間和堆空間

棧空間和堆空間

前面講的調用棧就是我們説的棧空間,存儲執行上下文用的,我們來看一段代碼:

function foo(){
    var a = "崔斯特"
    var b = a
    var c = {myName:"崔斯特"}
    var d = c
}
foo()

分析一下上面這段代碼變量的存儲,a和b是賦值着原始數據類型,所以他們會依次壓入棧中的執行上下文的變量環境,但是c賦值的是引用類型,這時候的情況就不一樣了。js引擎會把c、d分配到堆空間中,分配後會有一個堆地址,再把堆地址賦值給c。

可能現在你有疑問:把所有的數據存儲在棧空間不好麼?為什麼要維護棧空間和堆空間呢?

a. 因為js引擎要用棧來維護函數的執行上下文,在一個函數執行結束後,當前函數的執行上下文棧區空間會被全部回收,然後js引擎要離開當前的執行上下文,只需要將指針下移到下一個執行上下文就可以了。如果棧空間太大的話,會影響執行上下文切換的效率,進而影響到整個程序的執行效率!

b. 通常情況下棧空間不會設置很大,主要是存放一些原始數據類型。堆空間比較大,適合存放一些佔用空間較大的引用類型的數據。

內存模型視角的閉包

還是看上面的例子,foo函數的執⾏上下⽂銷燬時,由於foo函數產⽣了閉包,所以變量myName和test1沒有被銷燬,⽽是保存在內存中。這個過程在內存中是怎麼樣的呢?

  • js執行foo函數時,首先會編譯,編譯過程中遇到內部函數setName,js引擎還要對內部函數做一次詞法掃描,發現內部函數引用了foo函數中的myName變量,js引擎會判斷這是一個閉包,於是會在堆空間中創建一個"closure(foo)"對象(內部對象,js無法訪問)來保存myName。
  • 繼續掃面,發現setName函數內部還引用了test1,引擎又將test1添加到closure(foo)對象中,這時候對象就包含了兩個變量了。

當執行到foo函數時,閉包就產生了;當foo函數執行結束之後,返回的getName和setName⽅法都用“clourse(foo)”對象,所以即使foo函數退出了,foo函數執行上下文被銷燬了,“clourse(foo)”依然被其內部的getName和setNam法引用。所以在下次調用bar.setName或者bar.getName時,創建的執行上下文中就包含了“clourse(foo)”。

user avatar toopoo 頭像 dingtongya 頭像 front_yue 頭像 linx 頭像 hard_heart_603dd717240e2 頭像 u_17443142 頭像 shuirong1997 頭像 jiavan 頭像 xiaolei_599661330c0cb 頭像 yqyx36 頭像 nqbefgvs 頭像 hyfhao 頭像
點贊 49 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.