我對JavaScript中閉包的理解

之前對於閉包這個概念的理解都是模糊的,只是單純的知道閉包的作用:

可以在函數的外部訪問到函數內部的局部變量。
讓這些變量始終保存在內存中,不會隨着函數的結束而自動銷燬。
  而這幾天通過各種資料和博客的學習,自認對閉包的概念和原理有了一定的瞭解,所以來分享一下我的心得。如果文中有什麼不當之處,請多多諒解,並給與指正。謝謝!

###什麼是閉包?
在維基百科中的描述是:

在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。閉包在運行時可以有多個實例,不同的引用環境和相同的函數組合可以產生不同的實例。

上面的描述可能對於初學者來說有點晦澀。所以我們再來看看在JavaScript高級程序設計(第3版)中的描述:

閉包是指有權訪問另一個函數作用域中的變量的函數。

綜合這兩個解釋,我們大概的可以理解爲:閉包是可以在另一個函數的外部訪問到其作用域中的變量的函數。而被訪問的變量可以和函數一同存在。即使另一個函數已經運行結束,導致創建變量的環境銷燬,也依然會存在,直到訪問變量的那個函數被銷燬。就如下面代碼所示 

function add() {
	var sum = 0;
	function operation() {
		return sum = sum ? sum + 1 : 1;
	}
	return operation
}

var a = add();
console.log(a());//1
console.log(a());//2
console.log(a());//3
console.log(a());//4
a = null;
a = add();
console.log(a());//1

那麼爲什麼會這樣了?我們都知道在javaScriptS中,在函數的外部是不能訪問函數內部的變量的。並且隨着函數運行的結束,函數內部定義的變量也會被JavaScript的自動回收機制回收掉,並不會一直存在。那爲什麼閉包可以不受限制了,下面我們就來詳細討論下這個問題。

  想要理解JavaScript中閉包的概念。首先我們先要了解JavaScript中的作用域鏈。

當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。如果這個環境是函數,則將其活動對象(activation object)作爲變量對象。活動對象在最開始時只包含一個變量,即 arguments 對象(這個對象在全局環境中是不存在的)。作用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是作用域鏈中的最後一個對象。
————————————————
版權聲明:本文爲CSDN博主「王宜明」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_36276528/article/details/70049825

上面的一段是JavaScript高級程序設計(第3版)對作用域鏈的描述。簡單來說就是在函數中所能訪問的變量按照層級關係所組成的一條有着先後順序的鏈子。所以每個函數最先能訪問的變量(也就是在這條鏈子上最先能接觸到的)是當前函數的活動對象(就是在函數當中定義或重新賦值的變量),其次下一個能訪問的變量就是當前的函數所在的包含環境。(其包含環境一般是指外部函數或全局執行環境。但在ES6當中塊級代碼語句也有可能生成包含環境。)然後是下下個包含環境。這樣一層層的找下去,直到找到全局執行環境爲止。

  所以這樣我們就知道,能夠訪問一個函數A內部變量的除了這個函數A本身之外,其函數A所生成的包含環境中所在的函數B也可以訪問。而知道了這一點我們就可以很容易猜到閉包的原理。那就是既然函數A內部所在的其它函數B可以訪問到當前函數A的內部變量,那麼如果我們將其內部所在的其它函數B作爲返回值將其返回,並在函數A的外部用一個變量C來接收到這個返回值。那麼這樣,在函數A外部操作這個變量C時,實際上就是在操作函數A的返回值,也就是函數A所生成的包含環境中所在的內部函數B。而這個內部函數B是有權訪問到函數A的內部變量的,所以在函數A外部的變量也就可以訪問到函數A內部的變量。

  這樣就實現了在函數外部訪問到函數內部變量的操作。

  同時因爲閉包的這一作用,也就有了閉包的另一個作用:***被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。***也就是說被函數A外部的變量C所訪問到的函數A的內部變量,將會和變量C一同存在。即使函數A已經運行完畢,其內部變量也不會被JavaScript的自動回收機制回收掉。

  因爲此時,函數A的內部函數B依然在訪問函數A的內部變量。而內部函數B被返回給了函數A外部的變量C。此時變量C就代表了內部函數B,而又因爲變量C位於函數A的外部。所以函數A的結束運行,並不能影響到變量C的值,所以即使函數A結束運行。但變量C的值依然存在,而變量C的值函數A的內部函數B。而函數B是有權訪問函數A的內部變量的,所以就算函數A結束運行,按照邏輯變量C依然要可以訪問到函數A的內部變量。而這就與我們說知道的變量生命週期產生了衝突。

  然而只要瞭解了JavaScript的自動回收機制,就會發現這一切並不意外。

JavaScript 中最常用的垃圾收集方式是標記清除(mark-and-sweep)。當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲“進入環境”。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,因爲只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記爲“離開環境”。可以使用任何方式來標記變量。比如,可以通過翻轉某個特殊的位來記錄一個變量何時進入環境,或者使用一個“進入環境的”變量列表及一個“離開環境的”變量列表來跟蹤哪個變量發生了變化。說到底,如何標記變量其實並不重要,關鍵在於採取什麼策略。垃圾收集器在運行的時候會給存儲在內存中的所有變量都加上標記(當然,可以使用任何標記方式)。然後,它會去掉環境中的變量以及被環境中的變量引用的變量的標記。而在此之後再被加上標記的變量將被視爲準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最後,垃圾收集器完成內存清除工作,銷燬那些帶標記的值並回收它們所佔用的內存空間。
 

 

上面一段是JavaScript高級程序設計(第3版)中對於avaScript的自動回收機制的描述。其中有這麼一段從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,因爲只要執行流進入相應的環境,就可能會用到它們。所以當函數A的內部變量被內部函數B訪問時,此時函數A的內部變量就進入了函數B的環境中。而前面就說到,函數A的結束執行並不會影響到函數A外部的變量C,也就是內部函數B。所以內部函數B的環境會一直存在,直到變量C被註銷。因而函數A的內部變量也會一直存在到變量C的註銷爲止。

以上就是我個人對於閉包的理解。希望對大家有所幫助!

###最後要注意:

  • 閉包只能取得包含函數中任何變量的最後一個值。因爲別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。
  • 在確定絕對需要的地方纔使用閉包。因爲閉包到制變量不會被自動回收這一特性,可能會導致內存泄漏。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章