Ch. 5 範疇的 Closure
反高潮 (Anticlimactic):通常用作形容在電影,當劇情發到最後的高峰,要解決角色衝突時,卻沒有出現觀眾所期待的戲劇性高潮,使觀眾感到失望的情節設計效果。
頓悟
分享關於 Closure 的經驗?
自己遇過的情況是,前公司課程中有文法說明,會以音檔搭配動畫呈現,資料型態是 Array,包含文案和顯示時間,作法使用基本的 for 迴圈巡列,並等待給定的秒數後依序顯示。形成了一個 for 包住 setTimeout 的狀況,如果沒有用閉包將狀態存起來,就沒辦法呈現正確的結果。
基本事實
Closure 是函式記得並存取其語彙範疇的能力,甚至當函式是在語彙範疇之外執行時,也能正常運作。
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2
- 透過
baz()實際上執行了foo()中回傳的bar(),因此bar()是在宣告處的範疇之外被執行。 - 原則上
foo()被呼叫之後,已經不再被取用,應該要被垃圾回收掉,卻因為bar()而讓其範疇存活下來,這個指向該範疇的參考就是 Closure。 - 當我們將內層函式運送到範疇之外,Closure 會讓函式能存取它在編寫時期的語彙範疇,儘管它是在範疇之外被調用。
現在我能看到了
Closure 是隨處可見的,當調用一個回呼函式,把函式當成值到處傳遞,就很有可能看到函式使用 Closure。
下列函式中,若不是 timer 函式包圍了 wait () 的範疇,那麼它的範疇早就應該在 1 秒後消失。
function wait(message) {
setTimeout(function timer(){
console.log(message);
}, 1000);
}
wait('Hello, Closure');
IIFE 本身並沒有使用到 Closure,因為它並沒有在語彙範疇之外執行。
找找 Code 裡面的 Closure 吧!
迴圈與 Closure
Closure 經典案例:
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i); // 應該要印出 1, 2,...5,為什麼是 5 個 6
}, i * 1000 );
}
Callback 在迴圈執行完後才呼叫,這時 i 已經全都是 6 了。因為範疇共用,所以每次迭代都會覆蓋前一次的值。
解法一:使用 IIFE 並傳值讓其在每次迭代時建立一份拷貝、建立不同的函式範疇。
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
解法二:使用 let 每次建立不同的區塊範疇,並且會自動地將上一次的迭代結果作為這次的初始值。
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
模組
模組模式 (Module Pattern) / 揭露模式 (Revealing Module)
function CoolModule() {
var something = 'cool';
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(' ! '));
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
- 叫 CoolModule() 後會有一個模組實體被建立出來。
- 可以調用 return 物件中的方法,可以想像成是公開的 API。
- 透過 Closure,可以存取到 something 和 another 的內容,但同時保持資料的私有性。
行使模組模式的條件:
- 必須要有包裹的外層函式,並且至少被呼叫一次以建立實體。
- 包含函式至少要回傳一個內層函式,內層函式才能覆蓋私有範疇,藉此存取私有變數。
變體
- 透過 IIFE,可以將情況限制在只要一個實體的時候 (Singleton) (P.65)。
- 為回傳的物件取個名稱,就可以從內部修改實體,新增、移除或變更方法和特性 (P.66)。
現代模組
模組依存性載入器 (Module Dependency Loader) 就是將模組模式包裝成友善的 API。
var MyModules = (function Manager() {
// 變數 modules 是模組清單,
// 以模組的名字為 key,value 是個模組公開的 API
// (e.g. modules['bar'] 存放的是 hello)。
var modules = {};
// define 接收:
// name 模組名稱
// deps 相依模組陣列
// impl 函式,外層函式包裹內層函式,並且回傳內層函式以建立閉包
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]]; // (1)
}
modules[name] = impl.apply(null, deps); // (2)
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
// bar 沒有需要任何其他的模組...
// modules['bar'] = barImpl.apply(barImpl, []);
MyModules.define('bar', [], function barImpl() {
function hello(who) {
return 'Let me introduce: ' + who;
}
return {
hello: hello
};
});
// foo 需要 bar 模組...
// modules['foo'] = fooImpl.apply(fooImpl, ['bar']);
MyModules.define('foo', ['bar'], function fooImpl(bar) {
var hungry = 'hippo';
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome
};
});
var bar = MyModules.get('bar');
var foo = MyModules.get('foo');
bar.hello('hippo'); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
未來模組
ES6 模組系統會將一個檔案當作個別的模組。每個模組都能匯入其它模組或是匯出自己公開的 API。
ES6 的模組 API 是靜態的,不能夠在執行時期做變更。並且在編譯時期就及早指出可能的錯誤。
參考現有專案 (handlebar.js),討論模組情況。
函式庫 vs. 模組 vs. 框架。
複習
- Closure 的定義。
- 模組模式的兩個關鍵特徵。