JS執行上下文詳解(三):閉包

三、閉包

3.1 基礎

瞭解了詞法作用域鏈,接著我們就可以來聊聊閉包了。初學者對閉包的理解可能會是一道坎,剛接觸時會產生一些挫敗感,因為很難通過背後的原理來徹底理解閉包,從而導致學習過程中似乎總是似懂非懂。

當理解變量環境、詞法環境和作用域鏈等概念後,再來理解JavaScript中的閉包會容易很多,下面看看這段代碼:

<code>function foo() {    var myName = "IE";    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("Chrome");bar.getName();console.log(bar.getName());/<code>

熟練掌握JavaScript的同學一定能一眼看出答案,但是你真的清楚 ​getName​ 函數執行時的上下文嗎?下面我們來詳細分析一下這段代碼執行時的上下文環境:

首先我們看看當執行到 foo 函數內部的 return innerBar 這行代碼時的調用棧情況,如下圖:


瀏覽器原理系列 - JS執行上下文詳解(三):閉包


從上面的代碼可以看出,innerBar 是一個對象,包含了 getName 和 setName 的兩個方法(通常我們把對象內部的函數稱為方法)。你可以看到,這兩個方法都是在 foo 函數內部定義的,並且這兩個方法內部都使用了 myName 和 test1 兩個變量。

根據詞法作用域的規則,內部函數getName 和 setName 總是可以訪問它們的外部函數 foo 中的變量,所以當 innerBar對象被返回並賦值給全局變量bar時,雖然foo函數已經執行結束,但是getName 和setName函數總是可以使用foo 函數中的變量myName 和 test1。所以當foo函數執行完成後調用棧的狀態如下:


瀏覽器原理系列 - JS執行上下文詳解(三):閉包

從上圖可以看出,foo 函數執行完成之後,其執行上下文從棧頂彈出了,但是由於返回的 setName 和 getName 方法中使用了 foo 函數內部的變量 myName 和 test1,所以這兩個變量依然保存在內存中。這像極了 setName 和 getName 方法背的一個專屬揹包,無論在哪裡調用了 setName 和 getName 方法,它們都會揹著這個 foo 函數的專屬揹包。

之所以是專屬揹包,是因為除了 setName 和 getName 函數之外,其他任何地方都是無法訪問該揹包的,我們就可以把這個揹包稱為 foo 函數的閉包

看到這裡,是時候搬出閉包的正式定義:在 JavaScript 中,根據詞法作用域的規則,內部函數總是可以訪問其外部函數中聲明的變量,當通過調用一個外部函數返回一個內部函數後,即使該外部函數已經執行結束了,但是內部函數引用外部函數的變量依然保存在內存中,我們就把這些變量的集合稱為閉包。比如外部函數是 foo,那麼這些變量的集合就稱為 foo 函數的閉包。

下面我們接著分析,代碼執行到 bar.setName 方法中的 myName = 'Chrome' 時,JavaScript 引擎會沿著“

當前執行上下文–>foo 函數閉包–> 全局執行上下文”的順序來查找 myName 變量,你可以參考下面的調用棧狀態圖:

瀏覽器原理系列 - JS執行上下文詳解(三):閉包

從圖中可以看出,setName 的執行上下文中沒有 myName 變量,foo 函數的閉包中包含了變量 myName,所以調用 setName 時,會修改 foo 閉包中的 myName 變量的值。

同樣的流程,當調用 bar.getName 的時候,所訪問的變量 myName 也是位於 foo 函數閉包中的。

我們可以通過Chrome的開發者工具來看看閉包的情況,在getName函數內部加上斷點查看:


瀏覽器原理系列 - JS執行上下文詳解(三):閉包

從圖中可以看出來,當調用 bar.getName 的時候,右邊 Scope 項就體現出了作用域鏈的情況:Local 就是當前的 getName 函數的作用域,Closure(foo) 是指 foo 函數的閉包,最下面的 Global 就是指全局作用域,從“Local–>Closure(foo)–>Global”就是一個完整的作用域鏈。

3.2 閉包回收

通常,如果引用閉包的函數是一個全局變量,那麼閉包會一直存在直到頁面關閉;但如果這個閉包以後不再使用的話,就會造成內存洩漏。

如果引用閉包的函數是個局部變量,等函數銷燬後,在下次 JavaScript 引擎執行垃圾回收時,判斷閉包這塊內容如果已經不再被使用了,那麼 JavaScript 引擎的垃圾回收器就會回收這塊內存。

所以在使用閉包的時候,你要儘量注意一個原則:如果該閉包會一直使用,那麼它可以作為全局變量而存在;但如果使用頻率不高,而且佔用內存又比較大的話,那就儘量讓它成為一個局部變量。


分享到:


相關文章: