Function Factories 是透過呼叫執行一個函數,
這個函數執行完會回傳另一個幫我們做事的函數 ,
我們先來看看之前提過的一段程式碼:
function greeting(firstname, lastname, language) {
if(language === 'en') {
console.log('Hello ' + firstname + ' ' + lastname);
}
if(language === 'es') {
console.log('Hola ' + firstname + ' ' + lastname);
}
}
greeting('John', 'Doe', 'en');
greeting('John', 'Doe', 'es');
在 Console 中的結果如下:
這個結果如我們預期,在 Console 結果中分別呈現兩種不同的問候語,
再來看另外一個例子,
程式碼如下:
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');
在 Console 中的結果如下:
這種方式讓我們在呼叫函數的時候可以少傳一些參數,
傳入太多參數會影響程式碼可閱讀性,
透過 Function Factories 可提高程式碼可閱讀性,
來看看這背後發生了什麼事,
當程式碼執行時全域執行環境會被放進執行堆中執行,
並替函數 makeGreeting 與變數 greetEnglish 、greetSpanish 在全域執行環境中的變數環境設值,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
接著會執行呼叫函數 makeGreeting 並傳 'en' 當作參數指派給變數 greetEnglish 的那行,
呼叫函數會將函數的執行環境放進執行堆中執行,由於傳入的參數 language 是 'en',
所以在函數 makeGreeting 的執行環境裡的變數環境中的 language 設值成 'en',
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
當函數執行完閉執行環境從執行堆中被清除,但變數仍然存在記憶體中,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
接著會回傳一個匿名函數,
在這邊匿名函數尚未被執行,只是被創造並回傳給給變數 greetEnglish ,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
接著往下執行下一行也是呼叫函數 makeGreeting 但傳入不同參數 'es' ,
函數 makeGreeting 被放入執行堆中執行,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
這次呼叫函數 makeGreeting 傳入的參數 language 是 'es',
所以在函數 makeGreeting 的執行環境裡的變數環境中的 language 設值成 'es',
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
當函數執行完閉執行環境從執行堆中被清除,但變數仍然存在記憶體中,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
兩個不同執行環境的變數都還在記憶體中留存著,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
接著往下執行,
變數 greetEnglish 的值現在是回傳的匿名函數,
所以可以透過呼叫變數 greetEnglish 來呼叫回傳的匿名函數 ,
執行時將變數 greetEnglish 的值也就是回傳的匿名函數放進執行堆中執行,
也會在執行環境的變數環境裡替參數(變數) firstname 、lastname 設值成我們傳入的參數,
如下圖:
此時在匿名函數中找不到參數 language 所以會參考到外部詞彙環境,也就是函數 makeGreeting ,
所以進到範圍鏈參考外部環境時會找呼叫函數 makeGreeting 時參數傳入 'en' 的那個執行環境,
接著找到那個還存在記憶體中的變數 language 的值是 'en',
如下圖:
這時就會形成閉包將外部環境參考到的還存在記憶體中的變數包住,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
當執行函數 greetSpanish 時也是一樣,會先將函數 greetSpanish 的執行環境放進執行堆中執行,
也會在變數環境中替參數(變數) firstname 、lastname 設值,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
因為在函數 greetSpanish 中一樣找不到參數(變數) language ,所以會進到範圍鏈查找參數(變數),
函數 greetSpanish 的值也就是匿名函數的外部詞彙環境是第二次呼叫函數 makeGreeting 傳入參數 'es' 的那個執行環境,
因此在記憶體中找到變數 language ,變數 language的值是 'es',
在函數 makeGreeting 傳入參數 'es' 的那個執行環境已經從執行堆中被清除但變數 language 還在記憶體中,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
這會形成自己的閉包,
如下圖:
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 48 影片截圖
這就是整個背後執行的運作原理。
其實你可能早就已經使用過閉包了只是我們不知道背後的原理,
例如像是 setTimeout 以及事件的傳入函數(事件發生時執行的函數),
setTimeOut 的範例程式碼如下:
function sayHi() {
var greeting = 'Hi';
setTimeout(function() {
console.log(greeting);
}, 3000);
}
sayHi();
如果你到 Console 中看結果會發現要等 3 秒才會出現,
3
2
1
我們在 setTimeout 的方法中傳入兩個參數,
第一個參數傳入一個匿名函數,
之所以能夠傳入函數當作參數是因為 first class function 的特性,
函數在 JavaScript 中就是物件,
這個匿名函數可以直接寫在要傳入的第1個參數的位置是因為這個匿名函數在 setTimeout 方法中是函數表達式,
第 2 個參數是豪秒 我設定 3000 就是 3 秒後才會執行匿名函數,
在函數 sayHi 執行完後執行環境離開執行堆但變數 greeting 還在記憶體中存留著,
當匿名函數 3 秒後執行時會進到範圍鏈查找可以參考的外部詞彙環境還存留在記憶體中的變數 greeting ,
這會產生閉包 ,
接著如果你用過 jQuery 應該有使用過 click 事件,
程式碼如下:
$("button").click(function() {
// do something
});
jQuery 的內建 click 方法也接受一個匿名函數,這也是因為一級函數(first class function) 的特性,在 JavaScript 中函數就是物件,讓我們可以把函數當作參數傳入另一個函數中,
像這些當作方法的參數傳入的函數都叫作回呼 (callback),
你可能有個函數 a ,也有另一個函數 b,
將函數 b 當作函數 a 的參數傳入,
在函數 a 執行完時呼叫傳入的函數 b ,這就叫作回呼,
function a(callback) {
console.log('finished function a');
callback(); // 參數 callback 是我們在呼叫函數 a 時傳入的函數 b
}
function b() {
console.log('Hi');
}
a(b);
在 Console.log 的結果如下:
以上就是閉包與回呼(callback) 在真實專案上常常會遇到,
因此必須好好理解與實際動手練習過才會比較了解。