在JavaScript中函式是一等公民。所謂一等公民是指函式跟其他物件一樣,很普通,可以進行把函式存在陣列中、作為引數傳遞、賦值給變數等操作。當函式作為另一個函式的返回值在外部呼叫時,跟該函式在函式內部呼叫時可訪問的詞法作用域一樣,這種現象被稱為閉包。
一、什麼是閉包
閉包的定義有很多,比如:閉包是指有權訪問另外一個函式作用域中變數的函式。或者更本質的定義:函式物件可以通過作用域鏈關聯起來,函式體內部的變數都可以儲存在函式作用域內,也就是說函式變數可以隱藏於作用域鏈之內,看上去是函式將變數“包裹”了起來。個人比較傾向於一種通俗的定義:閉包就是函式能夠記住並訪問它的詞法作用域,即使當這個函式在它的詞法作用域外執行時。
如下程式碼所示,執行test函式返回print函式,在全域性作用域下執行print函式,print函式卻能記住自己的作用域,能夠引用其在定義時的外層函式test的區域性變數。
var a = 1;
function test (){
var a = 2;
function print (){
console.log(a)
}
return print
}
test()() // 2
複製程式碼
由於JavaScript中沒有塊級作用域的概念,因此常常用立即執行函式(IIFE)來模擬塊級作用域。
var a = 1;
(function IIFE(){
var a = 2;
console.log( a ); // 2
})();
console.log( a ); // 1
複製程式碼
從學術意義上來講,JavaScript中的每個函式都是閉包:它們都是物件,它們都關聯到作用域鏈。但是從閉包可以在詞法作用域外呼叫也能訪問詞法作用域的角度來說,IIFE並不是閉包。如下程式碼所示:在函式test執行時,查詢變數a是沿著作用域鏈逐級查詢的,並不能體現閉包的特性。
(function IIFE(){
var a = 1;
function test () {
console.log(a)
}
test() // 1
})();
複製程式碼
二、閉包的原理
當某個函式執行時,先複製其外層函式的作用域鏈(如果函式是在全域性環境中則作用域鏈中只有一個全域性物件的引用),賦值給一個特殊的內部屬性(即[[Scope]])。然後使用this、arguments和其它命名引數的值來初始化函式的變數物件,最後將該變數物件的引用加入到該函式的作用域鏈中。
當函式執行完之後,函式的作用域鏈上的會被刪除,相應的變數物件沒有了作用域鏈的引用就會被當做垃圾回收掉。但是閉包的情況卻不一樣,函式雖然執行完畢,但是函式返回了一個內部函式出去,該內部函式的作用域鏈上擁有對該函式變數物件的引用,因此函式雖然執行完畢,但該函式的變數物件並沒有被銷燬,依然可以通過返回的內部函式來訪問該函式變數物件上的變數。
需要特別注意的是:閉包只能取得包含函式中任何變數的最後一個值。在for迴圈中定義函式表示式尤其能體現出這一點。如下程式碼所示,我們希望test函式返回的函式陣列中存放的是可以列印其下標的函式,但是結果卻是全部數字10。原因在於我們錯誤的認為每次迴圈時都會對i進行一次複製,事實上巢狀的函式不會將作用域內的私有成員複製一份,也不會對所繫結的變數生成靜態快照。test函式返回的函式陣列中引用的都是同一個變數i,變數i被共享,迴圈結束時i的值為10,所以執行函式陣列中的任意函式結果都是列印出數字10。
function test () {
var arr = []
for(var i=0;i<10;i++){
arr[i] = function () {
console.log(i)
}
}
return arr
}
var print = test()
print[2]() // 10
複製程式碼
我們對程式碼加以改進,來避免資料共享的情況發生。在下面程式碼中,並不是直接將閉包賦值給陣列,而是定義了函式temp,將執行temp函式後的返回值賦給陣列。因為函式引數是按值傳遞的,所以每次呼叫temp時會複製一份實參i的副本,函式陣列中儲存的函式都有各自的變數i不同時間段的副本,打破了原本共享資料i的情況,因此能夠返回各自不同的值。
function test () {
var arr = []
for(var i=0;i<10;i++){
function temp (j) {
return function () {
console.log(j)
}
}
arr[i] = temp(i)
}
return arr
}
var print = test()
print[2]() // 2
複製程式碼
三、閉包的用途
閉包在JavaScript程式碼中無所不在,主要應用於模組模式以及函數語言程式設計中的柯里化。
1、模組模式
在ES6之前,JavaScript中並沒有定義用以支援模組的語言結構,但是可以利用閉包很輕鬆的實現程式碼模組化。在函式中定義的變數是函式私有的,在函式之外不能直接訪問以及修改函式內部的變數,但是通過函式返回的內部函式能夠訪問這些變數,返回的內部函式如同暴露在外界的共有介面一樣,這種模式被稱為模組模式。如下程式碼所示:
function module () {
var value = 0
function get () {
return value
}
function set (val) {
value = val
}
return {
get: get,
set: set
}
}
var test = module()
var modu = module()
console.log(test.get()) // 0
test.set(1)
console.log(modu.get()) // 0
console.log(test.get()) // 1
test.set(10)
console.log(modu.get()) // 0
console.log(test.get()) // 10
複製程式碼
在模組模式中,模組返回值可以是一個物件,也可以僅僅是一個內部函式。模組只是一個函式,所以它可以接收引數。從上面的程式碼可以看出函式每次執行返回的閉包是獨立的,相互不影響。一般模組在使用的時候採用單例模式,可以用IIFE來實現,如下程式碼所示:
var module =(
function module () {
var value = 0
function get () {
return value
}
function set (val) {
value = val
}
return {
get: get,
set: set
}
}
)()
console.log(module.get()) // 0
module.set(1)
console.log(module.get()) // 1
module.set(10)
console.log(module.get()) // 10
複製程式碼
綜上所述,模組要求兩個關鍵性質:1、作為模組的函式被呼叫執行。2、該函式的返回值至少用於一個內部函式的引用。
2、柯里化
柯里化是指把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。柯里化的好處在於提高了適用性,能夠實現引數複用的效果。如下程式碼所示:
function test (a,b,c) {
return a+b+c
}
console.log(test(1,2,3)) // 6
function _test (a) {
return function (b) {
return function (c) {
return a+b+c
}
}
}
console.log(_test(1)(2)(3)) // 6
複製程式碼
_test函式利用函式閉包來實現柯里化的效果,每次呼叫的函式閉包能夠訪問上次傳入的引數並訪問。例如lodash等庫封裝了通用柯里化的函式,傳入一個普通函式,返回一個同等功能的柯里化函式,這一部分會在本系列的後續文章詳述。
四、總結
閉包就是函式能夠記住並訪問它的詞法作用域,即使當這個函式在它的詞法作用域外執行時。函式執行完畢後,相應的變數物件沒有作用域鏈的引用就會被當做垃圾被回收,但是如果有閉包情況會變得不一樣,閉包的作用域鏈依然對外部函式的變數物件保持引用,因此外部函式的變數物件不會被銷燬,閉包依然能夠訪問外部函式的變數。
在JavaScript中沒有模組的語法(ES6之前),閉包可以用來實現模組模式。在函數語言程式設計中,柯里化十分常見,可以利用閉包來實現柯里化。
如需轉載,煩請註明出處:www.cnblogs.com/lidengfeng/…