1. 程式人生 > >【轉】js中的閉包

【轉】js中的閉包

js中為什麼要使用閉包?

先介紹一下全域性變數和區域性變數的優缺點:

全域性變數:在全域性環境下宣告的變數為全域性變數,全域性變數在任何地方都可訪問,且一直儲存在記憶體中只到應用程式退出(關閉網頁或瀏覽器)時才被銷燬。但是過多的宣告全域性變數容易造成全域性汙染,且全域性變數容易被修改。

區域性變數:在函式環境下宣告的變數為區域性變數,區域性變數僅在函式內部可訪問,當函式執行完畢時就會被銷燬。區域性變數不會造成全域性汙染也不容易被修改。

從上面可以看出全域性變數和區域性變數的優缺點剛好是相對的,閉包的出現正好結合了全域性變數和區域性變數的優點。閉包可使已經執行結束的函式中的區域性變數仍然留在記憶體中,且能被重複訪問使用。

閉包的定義是什麼?

閉包是指有權訪問另一個函式作用域中的變數的函式。建立閉包的常見方式就是在一個函式內部建立另一個函式。以一個函式為例:

js中的閉包

示例

加粗的兩行程式碼是內部函式(一個匿名函式)中的程式碼,這兩行程式碼訪問了外部函式中的變數propertyName。

js中的閉包

示例

在匿名函式從createComparisonFunction()中被返回後,他仍然可以訪問在createComparisonFunction()中定義的所有變數。更為重要的是,createComparisonFunction()函式在執行完畢後,其變數物件也不會被銷燬,因為匿名函式的作用域鏈仍然在引用這個變數物件。

什麼是作用域?

要理解什麼是作用域,要先理解什麼是執行環境

1.什麼是執行環境(執行上下文)

當代碼在JavaScript中執行的時候,程式碼在環境中被執行是非常重要的,它會被評估為以下之一型別來執行:

全域性程式碼:預設環境,程式碼第一時間在這兒執行。

函式程式碼:當執行流進入一個函式體的時候。

Eval程式碼:在eval()函式中的文字。

來看一個例子:

js中的閉包

示例

全域性環境由紫色邊框表示,還有三個不同的函式環境分別由綠色邊框,藍色邊框和橙色邊框表示。這裡只能有一個全域性環境,全域性環境可以被其他環境訪問。可以有很多的函式環境,每個函式都會建立一個新的函式環境,在新的函式環境中,會建立一個私有作用域,在這個函式中建立的任何宣告都不能被當前函式作用域之外的地方訪問。

2.執行環境的詳情

一個函式被呼叫就會建立一個新的執行環境。然而直譯器的內部,每次呼叫執行環境會有兩個階段:

1). 建立階段

- 當函式被呼叫,但是為執行內部程式碼之前:

- 建立一個作用域鏈。

- 建立變數,函式和引數。

- 確定this的值。

2). 啟用/程式碼執行階段

> - 賦值,引用函式,解釋/執行程式碼。

這意味著每個執行環境在概念上作為一個物件並帶有三個屬性

executionContextObj = {

scopeChain: { /* variableObject + all parent execution context's variableObject */ },

//作用域鏈:{變數物件+所有父執行環境的變數物件}

variableObject: { /* function arguments / parameters, inner variable and function declarations */ },

//變數物件:{函式形參+內部的變數+函式宣告(但不包含表示式)}

this: {}

}

看下面例子:

在呼叫foo(22)時,建立階段像下面這樣:

fooExecutionContext = {

scopeChain: { ... },

variableObject: {

arguments: {

0: 22,

length: 1

},

i: 22,

c: pointer to function c()

a: undefined,

b: undefined

},

this: { ... }

}

建立階段處理了定義屬性的變數名,但是並不把值賦給變數,不包括形參和實參。一旦建立階段完成,執行流進入函式並且啟用/程式碼執行階段,在函式執行結束之後,看起來像這樣:

fooExecutionContext = {

scopeChain: { ... },

variableObject: {

arguments: {

0: 22,

length: 1

},

i: 22,

c: pointer to function c()

a: 'hello',

b: pointer to function privateB()

},

this: { ... }

}

上述過程也可以印證了js中的變數提升,即變數和函式宣告會被提升到它們函式作用域的頂端。

理解了什麼是執行環境和作用域鏈之後,回到上文所講的閉包例項。

為什麼createComparisonFunction()函式在執行完畢後,其變數物件不會被銷燬。

js中的閉包

示例

在匿名函式從createComparisonFunction()被返回後,它的作用域鏈被初始化為包含createComparisonFunction()函式的變數物件和全域性變數物件。這樣,匿名函式就可以訪問在createComparisonFunction()中定義的所有變數。當createComparisonFunction()函式返回後,其執行環境的作用域鏈會被銷燬,但它的變數物件仍然會留在記憶體中;只到匿名函式銷燬後,createComparisonFunction()函式的變數物件才會被銷燬。下圖展示了呼叫compareNames()過程中產生的作用域鏈之間的關係:

js中的閉包

示例

解除對匿名函式的引用,只需compareNames=null即可,此時createComparisonFunction()函式的變數物件會被銷燬。

閉包的作用?

事實上,通過使用閉包,我們可以做很多事情。比如模擬面向物件的程式碼風格;更優雅,更簡潔的表達出程式碼;在某些方面提升程式碼的執行效率。這些需要感興趣的人自己去實踐和探索,此處不一一列舉。

閉包的缺點?

由於閉包會攜帶包含它的函式的作用域,因此會比其他函式佔用更多的記憶體。過度使用閉包可能會導致記憶體佔用過多,所以要慎重使用閉包。