1. 程式人生 > 其它 >掌握JavaScript中讓人迷惑的閉包

掌握JavaScript中讓人迷惑的閉包

目錄

目錄

JavaScript中閉包的定義

維基百科中關於閉包的定義

閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures)。

是在支援 頭等函式 的程式語言中,實現 詞法繫結

的一種技術。

閉包在實現上是一個結構體,它儲存了一個函式(通常是其入口地址)和一個關聯的環境(相當於一個符號查詢表)。

環境裡是若干對符號和值的對應關係,它既要包括 約束變數(該函式內部繫結的符號),也要包括 自由變數(在函式外部定義但在函式內被引用)。

閉包跟函式最大的不同在於,當捕捉閉包的時候,它的 自由變數 會在捕捉時被確定,這樣即便脫離了捕捉時的上下文,它也能照常執行。

MDN中關於閉包的定義

一個函式和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函式被引用包圍),這樣的組合就是 閉包closure)。也就是說,閉包讓你可以在一個內層函式中訪問到其外層函式的作用域。在 JavaScript 中,每當建立一個函式,閉包就會在函式建立的同時被創建出來。

看完上邊維基百科和 MDN 對閉包的解釋。是不是一臉懵逼?什麼是詞法繫結?頭等函式?自由變數又指的是什麼?

但我們從以上兩個官方的解釋來看,能統一得出一點結論:

閉包是一個結構體,其實現是函式。

想要理解閉包,首先需要知道兩個前提知識點:1. 函式的執行過程,2. 記憶體管理

JS中的一等公民——函式

何為一等公民?c語言中的抽象資料模型 類(class)、Java8之前的物件(Object)、這些都是在各自語言中的一等公民。它們都有一個特性:使用靈活,能夠在程式中無限使用的物件,作為函式時的引數傳遞,以及作為函式的返回值。

在 JavaScript 中,函式是非常重要的,並且是一等公民:

  1. 函式可以作為另外一個函式的引數。
  2. 函式頁可以作為另外一個函式的返回值。

JS中函式的使用是非常靈活的,具體我們用幾個案列來解釋:

  1. 將函式作為另外一個函式的引數。
function foo(baz){
	baz()
}
function bar(
	console.log('bar')
)

foo(bar)
  1. 函式作為函式的返回值,js 允許函式內部再定義函式。
function foo() {
	function bar() {
		console.log('bar')
	}
	
	return bar
}

var fn = foo()
fn()
  1. 陣列中的高階函式
/*
高階函式:一個函式如果接收另一個函式作為引數,或者該函式會返回另外一個函式作為返回值的函式,那麼這個函式就稱之為是一個高階函式。
*/
var nums = [10, 7, 9, 100, 11]
var newNums = nums.filter((item) => {
  return item % 2 === 0 // 偶數
})

console.log(newNums) // [10, 100]
console.log(nums) // [10, 5, 11, 100, 55]

有了以上對函式的理解。我們再來看看 js中的記憶體管理:垃圾回收機制。

JS的記憶體管理

記憶體結構

JavaScript會在 定義變數時 為我們分配記憶體。

但是記憶體的分配方式是一樣的嗎?

JS 對於基本資料型別記憶體的分配會在執行時,直接在棧空間進行分配;

JS 對於複雜資料型別記憶體的分配會在堆記憶體中開闢一塊空間,並將這塊空間的指標返回值變數引用;

例如以下程式碼在記憶體中的結構:

var name = "mjy"
var age = 18

var obj = {
	name: 'mjy',
	job: 'coder'
}

function foo() {
	console.log('hello')
}

記憶體管理:垃圾回收機制

因為記憶體大小的有限,所以當記憶體不再被需要的時候,我們就要對其進行記憶體釋放。以便騰出更多的空間。

手動管理記憶體的語言中,如 c:通過 malloc 來進行記憶體的申請,通過 free 進行記憶體的釋放。

但是這種方式十分的低效麻煩,而且一不小心使用完後就會忘記釋放。

所以大部分現代程式語言都有自己的垃圾回收機制

垃圾回收的英文是:Garbage Collection,簡稱 GC。

對於不再使用的物件,我們都稱之為垃圾。它需要被回收掉。

在 JavaScript 引擎中,有著自己的垃圾回收演算法:

常見GC演算法

1. 引用計數法:

​ 當一個物件有一個引用指向它時,那麼這個物件的引用就 +1,當一個物件的引用為 0 時,這個物件就可以被銷燬掉,也就是可以被GC回收掉。

​ 這個演算法有一個很大的弊端:迴圈引用,導致無法被正常回收。

2. 標記清除法(運用最廣):

​ 這個演算法是設定一個根物件(root object),垃圾回收器會定期從這個根物件開始,找所有從根物件開始有引用到的物件,對於那些沒有引用到的物件,就認為是不可達(不可用)的物件。GC就會對其進行回收。

​ 這個演算法可以很好的解決迴圈引用產生的問題:

在有了以上兩個知識點(函式執行過程、記憶體管理)的瞭解後,我們就可以正式得來了解閉包了

JavaScript中函式的執行過程

要理解閉包,首先就要理解JavaScript中的函式,以及JavaScript函式的執行過程。可以看我之前寫過的這篇文章 理解JavaScript函式執行過程,作用域鏈。

我們先來看這樣的一段程式碼,在 js 引擎中的執行過程。

function foo() {
    var name = "mjy"
    var age = 18
}

function test() {
 	console.log('test')   
}

foo()
test()

函式解析時

在 js 引擎解析程式碼時,GO 物件被建立。程式碼依次解析,當解析到 foo、test 函式時,會在堆記憶體中為其開闢一塊記憶體空間,記憶體空間中存放該函式的父級作用域(parentScope)和函式執行體。並將開闢的記憶體空間地址以址返回的形式儲存在 GO 物件的 foo 與 test 中。

函式執行時

開始執行程式碼,函式執行上下文入棧,並生成函式AO物件,依次執行函式中的程式碼。

函式物件銷燬

當foo函式執行完畢,函式執行上下文出棧,foo函式的物件AO銷燬。

閉包的產生

在理解了上面函式的執行過程後,我們來看下面這樣一個函式。

function foo() {
	var name = "mjy"
	var age = 18

	function bar() {
		console.log(name)
		console.log(age)
	}
	
	return bar
}

var fn = foo()
fn()

當foo函式執行時,foo函式建立 AO 物件。foo中程式碼進行解析(當函式確定要被執行時,foo中的程式碼才會進行解析),foo函式體程式碼解析到bar函式,並在記憶體中為bar開闢記憶體空間。

解析完畢,執行 return bar 程式碼,將bar函式的記憶體空間地址,以址返回的形式賦值到變數fn中,fn = foo()

當foo函式執行完畢時,本該銷燬的fooAO物件並不會被消耗(也就是不會被GC回收掉)。因為此時 foo 函式的 AO 物件,在根物件GO中是可達的,有變數在指向它(不清楚這一步可以回頭看:GC演算法--標記清除法)。

這也是閉包會產生記憶體洩漏的原因。

foo函式執行完畢出棧後,test函式入棧。開始執行生成test函式AO物件,並解析後依次執行bar函式體中的程式碼:console.log(name)

bar沿著函式作用域鏈查詢要列印的值 name 首先會在自身AO物件中查詢。如果未查詢到,將會沿著父級作用域繼續查詢,在父級作用域foo函式的AO中物件查詢到 name 屬性,將其列印。

理解總結

以上就是一整個閉包的形成過程了。我們現在再回過頭來看 維基百科、MDN中對閉包的定義:

閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包>>。(function closures)。

是在支援 頭等函式 的程式語言中,實現 詞法繫結 的一種技術。

閉包在實現上是一個結構體,它儲存了一個函式(通常是其入口地址)和一個關聯的環境(相當於一個符號查詢表)。

環境裡是若干對符號和值的對應關係,它既要包括 約束變數(該函式內部繫結的符號),也要包括 自由變數(在函式外部定義但在函式內被引用)。

閉包跟函式最大的不同在於,當捕捉閉包的時候,它的 自由變數 會在捕捉時被確定,這樣即便脫離了捕捉時的上下文,它也能照常執行。

一個函式和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函式被引用包圍),這樣的組合就是 閉包closure)。也就是說,閉包讓你可以在一個內層函式中訪問到其外層函式的作用域。在 JavaScript 中,每當建立一個函式,閉包就會在函式建立的同時被創建出來。

現在看來是不是就很清晰了。

在理解了閉包後,你會發現 維基百科 和 MDN 中對閉包的解釋定義是十分準確的(官方喜歡用名詞來解釋名詞,因為這樣才能更準確得表達)。

這裡摘抄coderwhy老師對閉包的理解:

  • 一個普通的函式function,如果它可以訪問外層作用域的自由變數。那麼這個函式就是一個閉包。
  • 從廣義的角度來說:JavaScript 中的函式都是閉包。
  • 從狹義的角度來說:JavaScript 中的一個函式,如果訪問了外層作用域的變數,那麼它就是一個閉包。