在「閉包」這一關,我一直有一種似懂非懂,玄之又玄的感覺。
MDN上對「閉包」的定義:
「閉包為函式的組合、還有該宣告函式的作用域環境。這個環境包含閉包建立時,所有位於該作用域的區域變數。」
每個字都看得懂,但是合起來是甚麼意思?
唉!我們重新來看一下函式的寫法:
小龍女在絕情谷底養的玉峰,飛到周伯通住的百花谷,要如何分辨一班的蜜蜂與小龍女養的玉峰呢?當然是看看翅膀上有沒有寫:「我在絕情谷底」,有寫的就是玉蜂。
這是一般函式的寫法:
var bee="蜜蜂";
function flyOut(){
var bee = "玉蜂";
return `${bee}翅膀上有寫「我在絕情谷底」`;
}
flyOut();
//"玉蜂翅膀上有寫「我在絕情谷底」"
如果我們在flyout()再內嵌一個inner函式,這時直接呼叫flyout(),出來的結果會是undefined。
var bee="蜜蜂";
function flyOut(){
var bee = "玉蜂";
function inner(){
return `${bee}翅膀上有寫「我在絕情谷底」`;
}
}
flyOut();
//undefined 沒有回傳值
但是如果我們再flyout那一層,加上「return inner();」,會回傳「"玉蜂翅膀上有寫「我在絕情谷底」"」。
var bee="蜜蜂";
function flyOut(){
var bee = "玉蜂";
function inner(){
return `${bee}翅膀上有寫「我在絕情谷底」`;
}
return inner();
}
flyOut();
//"玉蜂翅膀上有寫「我在絕情谷底」"
再來把return inner()的小括號拿掉。
var bee="蜜蜂";
function flyOut(){
var bee = "玉蜂";
function inner(){
return `${bee}翅膀上有寫「我在絕情谷底」`;
}
return inner;
}
flyOut();
//ƒ inner(){
return `${bee}翅膀上有寫「我在絕情谷底」`;
}
結果回傳的是inner()的程式碼:
ƒ inner(){
return `${bee}翅膀上有寫「我在絕情谷底」`;
}
再更進一步,在外層用變數outBee來存取flyOut():
var bee="蜜蜂";
function flyOut(){
var bee = "玉蜂";
function inner(){
return `${bee}翅膀上有寫「我在絕情谷底」`;
}
return inner;
}
var outBee = flyOut();
outBee();
//"玉蜂翅膀上有寫「我在絕情谷底」"
console.log(outBee())
還記得「切分變數最小的範圍是function」這句話嗎?
inner()被內嵌在flyOut()之內,所以inner()裡的變數能夠存取的範圍就是flyOut()跟全域的範圍,它在flyOut()裡面找到了bee = "玉蜂",就不會再往外層去找。這種訪問機制就是「作用域鍊(Scope chain)」
JavaScript 引擎的回收機制會釋放不再使用的記憶體,清空不再使用的變數,但閉包為了保留函式記得和存取其執行環境的能力,就會予以保留,不做記憶體回收。所以當程式執行完var outBee = flyOut();這一行,原本應該被記憶體釋放掉的flyOut()裡面的變數bee變成了「自由變數」,還是可以拿來運算。
這種可以適用自由變數的函式,就是「閉包」。
雖然 outBee 位於 flyOut()函式所定義的範疇之外,但由於閉包的緣故, 所以能正常執行inner()函式,並存取到 bee 的值,進而執行出"玉蜂翅膀上有寫「我在絕情谷底」"的結果。
所以在flyout()的函示內部回傳inner函式的同時,除了傳回程式碼之外,也回傳了內部函式建立時的變數值bee="玉蜂",連同執行環境一起被回傳了。
嗯!這樣在絕情谷外的楊過就可以依據蜜蜂身上的資訊找到小龍女了!XD
這就是一種「閉包」的資料結構,包含函式及函式被建立時的當下環境。
許國政先生在《0 陷阱!0 誤解!8 天重新認識 JavaScript!》一書中:
「當你在呼叫函式的以前,範圍鍊就已經被建立了。因此我們可以在函式(outer)裡面「回傳」另一個內部函式(inner)給外層的範圍,使得外層也可以透過『範圍鍊』取得內部的變數(msg)。」
這句話直白易懂,是大神才寫得出來的!
所以我們可以透過「閉包」的方式,呼叫「函式」內的「函式」,我們可以把變數封裝在函式中,避免變數汙染全域環境,而且可以重複存取叫用函式及其內部環境。許多框架也是透過這種方式運作的。