在正式進入之前,我們先來看一段code:
function greet(whattosay) {
return function(name) {
console.log(whattosay + ' ' + name);
}
}
greet('Hi')('Tony')
// 輸出會是:Hi Tony
好像有點難懂,我們換另一種寫法:
function greet(whattosay) {
return function(name) {
console.log(whattosay + ' ' + name);
}
}
var sayHi = greet('Hi');
sayHi('Tony');
//輸出會是: Hi Tony
看起來好像很合理,但如果仔細一想,有一個疑問:為什麼sayHi
會知道whattosay
這個參數。
我的想法是:whattosay
是在我們called
greet function
時創建的,而當greet這個function執行完成後參數whattosay
應該會從execution stack
離開才對。為什麼sayHi
還能找到呢?
這就是我們要學的closures
所帶來的結果。
先來一段MDN的說明:
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
簡單來說,閉包是指當內部 function 引用了外部 function 的變數時,這些變數會被保存在閉包內,使得即使外部 function 執行完畢後,內部函式依然可以訪問和操作這些變數。
一般而言,當function被執行後,function 內的資料就會被銷毀,從而釋放記憶體空間。
但有了這項特性後,我就可以利用這個特性來保存我們想要的資料。
當我們執行整段code時,我們知道整段code的global execution context
會被建立。
當我們來到 var sayHi = greet(’Hi’)
時,他會invokes
greet這個function,創建新的execution context
。
當我們要執行function greet(whattosay)
時,javaScript引擎會注意到這裡有一個parameters
(參數),因此把他放到了execution context
裡。
當再往下執行後,發現了return
,因此回傳了後面整段function,所以整個greet()
就會從stack
彈出。
但要記得,我們說過每個execution context
在記憶體裡都會有一個空間,在正常情況下,JavaScript引擎會透過garbage collection
來清除內容,但在execution context抽離的當下,雖然execution context已經不在了,但裡面的變數還是儲存在那個記憶體位置。
當我們繼續往下執行到sayHi('Tony')
時,我們建立了一個給匿名函式的execution context,同時裡面帶有變數name。
當我們執行這個這個匿名函式的console.log(whattosay + ' ' + name)
時,JavaScript引擎就會過scope chain
的方式來尋找whattosay
這個變數。這時候雖然我們的greet這個function的execution context
已經不在了,但其實在這個記憶體位置仍然留有參照(reference),所以在greet function裡面所建立的函式仍然可以找得到whattosay
這個變數。
到這邊就是整個closure的底層原理。
function buildFunctions() {
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(
function() {
console.log(i);
}
)
}
return arr;
}
var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();
看到第一眼本來很直覺的想要說答案是0,1,2,但在仔細看了一遍後注意到arr裡面放的是function而不是number。
所以console.log(i)
並不會在被執行,而是當我們程式走到fs[0]()
時,他才會去執行console.log(i)
,所以透過scope chain
去找到i的值。
在buildFunctions
這個function執行for迴圈時,每當執行一次,就會把 function( ){console.log(i)} 儲存到陣列中,但要注意的是這時候這個被儲到陣列中的function並沒有執行(invoke),而是只是儲存在裡面而已,因為它沒有透過括號 ( ) 來執行;然後 i 會繼續累加,當 i 累加到3的時候,因為不符合 i < 3 所以會跳出迴圈。因此i就會是等於3,而arr會是長這樣:
arr = [f0, f1, f2]
因此當我們透過scope chain
去找i時就會得到i = 3,之後再帶入到程式碼裡面就會得到答案3。由於f0、f1跟f2都擁有同樣的outer environment reference,因此答案會是:3,3,3。
那如果這時候我們要讓他輸出的結果變成 0, 1, 2時,我們該怎麼做?
這邊提到了2個做法,第一個是使用let
,第二個就是使用IIFEs
。
先看let
:
function buildFunctions() {
var arr = [];
for (var i = 0; i < 3; i++) {
let j = i;
arr.push(
function() {
console.log(j);
}
)
}
return arr;
}
透過let,可以讓每次跑的迴圈都建立在一個新的記憶體位置,因此最後指到的地方會是不一樣的,於是可以輸出0, 1, 2的結果。
使用IIFE
就稍微複雜了:
function buildFunctions() {
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(
(function(j) {
return function() {
console.log(j);
}
}(i))
)
}
return arr;
}
(function(j){...})(i)
因為這段是IIFE,所以他會直接被執行並且會把變數i帶到function裏面,這也就導致在一開始原本一樣的outer environment reference
變成不一樣,因此當我們要再去outer environment reference
找參數時,就會因為參數的值不同,得到的輸出就不會再是都一樣的。
function makeGreeting(language) {
return function(firstname, lastname) {
if (language === 'en') {
console.log('Hello ' + firstname + ' ' + lastname);
}
if (language === 'es') {
console.log('Hola ' + firstname + ' ' + lastname);
}
}
}
var greetEnglish = makeGreeting('en');
var greetSpanish = makeGreeting('es');
greetEnglish('John', 'Doe');
greetSpanish('John', 'Doe');
閉包的使用當然有很多種,例如上面這段code,雖然比前一個練習更複雜,但我們只要記得:
每執行一次函式,就會產生一個新的execution context,即使有多個參數值被儲存在記憶體中,JavaScript引擎會自己找到屬於該execution context的變數。
closure是JavaScript引擎的一種特性,並不是說你需要去創造它或執行它。透過closure這樣的特性,我們可以確保當我們在執行function的時候,JavaScript引擎能夠找到其相對應的變數,也就是說,不論某一個function是不是已經執行完畢,是不是已經抽離execution stack,JavaScript引擎仍然可以找到外面的變數。
callback function指的是在一個function(例如,funcA)中放入另一個function(例如,funcB),而且當這個funcA執行完後會觸發funcB的執行。
// 為了確保先執行 funcA 再執行 funcB
// 我們在 funcA 加上 callback 參數
var funcA = function(callback){
var i = Math.random() + 1;
window.setTimeout(function(){
console.log('function A');
// 如果 callback 是個函式就呼叫它
if( typeof callback === 'function' ){
callback();
}
}, i * 1000);
};
var funcB = function(){
var i = Math.random() + 1;
window.setTimeout(function(){
console.log('function B');
}, i * 1000);
};
// 將 funcB 作為參數帶入 funcA()
funcA( funcB );
Closure的觀念非常重要,在重新整理過一遍筆記後,發現自己還是有不是那麼了解的地方。
因此又重新看了一遍**JavaScript: Understanding the Weird Part
。**
結果字幕沒有中文了…還好只是複習一下自己忘記的地方,不然完全不懂的情況下要我聽英文看英文字幕學習我應該辦不到….