Closure 是 ECMAScript 代表性功能,也是 Functional Programming 基礎,很多神妙的 FP 機制都是由 Closure 展開,善用 Closure 將使得程式碼更為精簡,可讀性更高,也更容易維護。
Version
macOS Catalina 10.15
VS Code 1.38.1
Quokka 1.0.254
ECMAScript 5
ECMAScript 2015
IIFE
for(var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
})(i);
}
因為 var
只有 function scope,而 setTimeout()
又是 asynchronous function,為了避免 setTimeout()
的 callback 取得 i
都是 5
,特別使用 IIFE 為 setTimeout()
加上 anonymous function,當 i
傳入 anonymous function 後,因為 closure 形成了新的 function scope 將 i
鎖起來,如此 console.log()
透過 lexical scope 取得的 i
就是被 鎖住
的 i
,而不是被 var
所遞增的 i
。
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000 * i);
}
在 ES6 有簡單解法,只需將 var
換成 let
即可,也不須 IIFE 與 closure,因為 let
為 block scope,當傳入 setTimeout()
的 callback 時,因為有 {}
已經將 i
鎖住,而不是被 let
所遞增的 i
。
Callback Factory
let data = [1, 2, 3];
data.map(x => x * 2); // ?
data.map(x => x * 2 + 1); // ?
ECMAScript 提供很多 higher order function,如 Array.prototype.map()
與 setTimeout()
,我們會以 arrow function 傳入 callback。
let data = [1, 2, 3];
let makeCallback = (a, b) => x => a * x + b;
data.map(makeCallback(2, 0)); // ?
data.map(makeCallback(2, 1)); // ?
若 callback 有重複,或者類似 callback 不斷出現,則可藉由 closure 抽出新 function,藉由傳入不同 argument 產生不同 callback。
Method Factory
let obj = {
fn1: x => 2 * x + 3,
fn2: x => 3 * x + 1,
};
obj.fn1(1); // ?
obj.fn2(1); // ?
傳統 OOP 是 method 宣告在 class 內,但 ECMAScript 的 method 是以 function 掛在 object literal 上。
let makeFn = (a, b) => x => a * x + b;
let foo = {
fn1: makeFn(2, 3),
fn2: makeFn(3, 1),
};
foo.fn1(1); // ?
foo.fn2(1); // ?
當使用 object literal 定義 method 時,若 method 有重複,或者類似規則的 method 不斷出現,則可藉由 closure 抽出新 function,藉由傳入不同 argument 產生不同 method。
此技巧在 Vue 常常使用,如 Vue 的 computed 本質是 function,若發現眾多 computed 有類似規則,可抽出新 function 產生 computed
Partial Application
let fn = (x, y, z) => 2 * x + 3 * y + z;
fn(10, 3, 2); // ?
fn(10, 1, 2); // ?
fn(10, 4, 2); // ?
fn
為多 argument 的 function,但實務上發現第 1 個 argument 都一樣,只有第 2 個與第 3 個 argument 在改變。
let fn = x => (y, z) => 2 * x + 3 * y + z;
let fn_ = fn(10);
fn_(3, 2); // ?
fn_(1, 2); // ?
fn_(4, 2); // ?
將第 1 個 argument 傳入 fn()
即可,會傳回新的 fn_()
。
然後只需將第 2 個與第 3個 argument 傳入 fn_()
即可得到結果,不需再重複傳入第一個 argument。
Partial Application
Function 若只傳部分 argument 進去,則會回傳新的 function;直到所有 argument 都傳遞完全才開始求值
Currying
let fn = (x, y, z) => 2 * x + 3 * y + z;
fn(1, 1, 1); // ?
fn(1, 1, 2); // ?
fn(1, 2, 2); // ?
fn(1, 2, 3); // ?
fn(1, 3, 4); // ?
傳統會以多 argument 建立 function。
let fn = x => y => z => 2 * x + 3 * y + z;
let fn1 = fn(1)(1);
let fn2 = fn(1)(2);
fn1(1); // ?
fn1(2); // ?
fn2(2); // ?
fn2(3); // ?
fn(1)(3)(4); // ?
Partial application 固然靈活,但只能針對特定 argument 組合回傳 function,若所有 funtion 都只有 1 個 argument,則 argument 的排列組合是最靈活的。
Currying
將原本 n 個 argument 的 function 改用 n 個 1 個 argument 的 function 所構成
Encapsulation
let obj = {
name: 'Sam',
sayHello: function() {
return `Hello ${this.name}`;
},
};
obj.sayHello(); // ?
使用 object literal 建立 object 時所有的 property 都是 public,完全沒有任何 encapsulation 可言,該如何實現如 OOP 有 private field 呢 ?
let obj = (function(name) {
return {
sayHello: () => `Hello ${name}`,
}
}('Sam'));
obj.sayHello(); // ?
使用 Closure + IIFE,name
可如 OOP 的 constructor 般傳入設定初始值,重點是 name
被封裝在內部,外界無法更改,sayHello()
可透過 lexical scope 讀取 name
,並藉由 closure 鎖住 name
。
let obj = (name => ({ sayHello: () => `Hello ${name}`}))('Sam');
obj.sayHello(); // ?
亦可使用 ES6 的 arrow function 實現。
let obj = (function(name) {
let addTitle = name => `Mr. ${name}`;
let title = addTitle(name);
return {
sayHello: () => `Hello ${title}`,
}
}('Sam'));
obj.sayHello(); // ?
也能在 anonymous function 增加 variable 與 function,相當於 OOP 的 private field 與 private method。
let obj = (name => {
let addTitle = name => `Mr. ${name}`;
let title = addTitle(name);
return {
sayHello: () => `Hello ${title}`,
}
})('Sam');
obj.sayHello(); // ?
亦可使用 ES6 的 arrow function 實現。
Memorization
let isPrime = value => {
if (!isPrime.cache) isPrime.cache = {};
if (isPrime.cache[value]) return isPrime.cache[value];
let prime = value !== 1;
for(let i = 2; i < value; i++) {
if (value % 2 === 0) {
prime = false;
break;
}
}
return isPrime.cache[value] = prime;
};
isPrime(13); // ?
判斷質數
是很耗 CPU 的運算,我們希望能將運算過的結果 cache 下來,可直接對 isPrime()
新增 cache
property,若 cache 有值則直接回傳,若 cache 無值才運算。
let isPrime = (() => {
let cache = {};
return value => {
if (cache[value]) return cache[value];
let prime = value !== 1;
for(let i = 2; i < value; i++) {
if (value % 2 === 0) {
prime = false;
break;
}
}
return cache[value] = prime;
};
})();
isPrime(13); // ?
Memorization 另一種實現方式是利用 closure,這種方式的優點在於 encapsulation,cache 不會暴露在外被修改。
Conclusion
- Closure 是 FP 的基礎,如此才能回傳新的 function,並以新的 function scope 保留原本的 lexical scope
- 透過 closure + IIFE,也能以 function 實現 OOP 的 encapsulation