今天來看看closure閉包
先來看看以下程式碼:
function say(whattosay){
return function(name){
console.log(whattosay + ' ' + name);
}
}
設定一個函式陳述句,在裏頭用函式表示式回傳一個函式,並利用範圍練scope chain
的特性放入whattosay,裏頭這個回傳函式沒有宣告whattosay,於是它會外部(參照)查找,去找設定這個函式的say函式參數whattosay。
關於外部參照
,可以參考這天的筆記。
當我們呼叫函式say,會得到一個值,這個值是從函式say裡面return返回的另一個函式。我們可以帶入參數,這樣呼叫函式裡的函式:
say('Hello')('Simon');
目前這樣還OK
接著修改一下程式碼,設定一個變數去接(指向)函式say的回傳值:
var talk = say('Hello');
talk('Simon');
現在talk這個變數,會指向函式say執行時回傳的函式。
再透過呼叫talk,並傳入參數字串Simom。
結果是:
一樣可以執行,乍看很合理,但好像有點怪怪的?
影片看到這邊我還不曉得為什麼,後面看講者的解釋才驚覺,對啊,為什麼?
talk(指向的)函式居然能外部參照到whattosay!
說明
當函式被呼叫,會在全域執行環境創造這個函式的執行環境,在函式執行環境裡執行它的程式,當函式結束,其函式執行環境也結束,而這個例子,程式碼全貌是這樣:
function say(whattosay){
return function(name){
console.log(whattosay + ' ' + name);
}
}
var talk = say('Hello');
talk('Simon');
whattosay被創造在var talk = say('Hello');
這段的say('Hello')
,say函式執行其程式,然後回傳出裏頭的函式給變數talk指向,接著結束say函式執行環境,就算它的arguments
有'Hello'
,它就是結束了。
say函式離開JS的執行堆,每個函式都有自己的記憶體空間,函式內的變數與其他程式被設定在那裏,當函式的執行環境結束,JavaScript引擎預設會清除這個記憶體內容,這稱為garbage collection
垃圾回收,即是將多餘的記憶體內容清掉,回收記憶體空間。
理論上say函式的記憶體空間都被清除,whattosay自然也不在電腦記憶體裡了,say函式執行環境結束了,記憶體也被回收,但裏頭的whattosay卻沒有被電腦回收。
透過變數talk去執行其指向的函式,創造一個函式執行環境。當函式發現沒有whattosay變數給它使用,便向外參照,然後居然可以找到記憶體裡的whattosay!
即便say函式已經結束了,talk的函式依然可以找到對應的外部變數,這種現象稱之為closure
閉包
這是JS的特性之一:即便這個變數所在的執行環境已經結束,jS引擎仍會確保,其它函式執行時可以找到它所需的變數。
接著來看看另一個閉包範例:
function buildFunctions(){
var arr = [];
for(var i=0; i<3; i++){
arr.push(function(){
console.log(i);
});
}
return arr;
}
建立一個函式buildFunctions,裏頭宣告陣列arr,並有個for迴圈陳述句,每次迴圈都會
把:
function(){
console.log(i);
}
塞入arr陣列裡,最後把arr回傳出來。
承接程式碼內容,接著
var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();
宣告變數fs並指向呼叫呼叫buildFunctions()
後回傳的值(陣列arr),接著個別呼叫fs指向的陣列arr,序號0、1、2的函式。
執行結果是什麼?會是印出0、1、2
或1、2、3
嗎?
不對,因為fs[0]、fs[1]、fs[2]現在都是:
function(){
console.log(i);
}
這三個函式在buildFunctions的迴圈裡被創造,並未執行,所以函式內都是console.log(i);
結果只會是印出三個一樣的i
。接著要找i值,因為執行環境的不同,i
理論上應隨著buildFunctions
函式的結束而被記憶體清除,但因為閉包的特性,i
被留下在記憶體裡,而i
最後更新的值是3,所以結果是:
印出3個3
那麼,有辦法修改程式,讓其印出3個不同的連號嗎?
這裡有兩種方法可以處理:
let
宣告變數j,let
強調在{}
的區塊執行環境,每次let
都會保留j在不同的記憶體位置,利用閉包取值時結果自然不一樣囉。function buildFunctions(){
var arr = [];
for(var i=0; i<3; i++){
let j = i;
arr.push(function(){
console.log(j);
});
}
return arr;
}
var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();
結果是:
IIFE
:在buildFunctionsc函式內的迴圈,裏頭的push函式改成立即執行函式,每次立即函式被創造出來就立刻執行,而且每次立即函式的執行環境都不一樣,利用此方法可以保留i的值在不同的記憶體空間,利用閉包取值時結果自然不一樣囉。function buildFunctions(){
var arr = [];
for(var i=0; i<3; i++){
arr.push(
(function(j){
return function(){
console.log(j);
}
})(i)
);
}
return arr;
}
var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();
結果是:
小結
閉包是JS語言中的一個重要特性,網路上有不少閉包文章,有興趣也可以參考MDN的運用說明。
今天的筆記內容可以參照Udemy課程:JavaScript 全攻略:克服JS 的奇怪部分4-46、4-47