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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *