反高潮 (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 的定義。
- 模組模式的兩個關鍵特徵。