點燈坊

失くすものさえない今が強くなるチャンスよ

實務上如何使用 Closure ?

Sam Xiao's Avatar 2019-10-09

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

closure008

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

closure009

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。

closure010

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。

closure000

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 上。

closure011

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

closure001

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 在改變。

closure012

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 都傳遞完全才開始求值

closure002

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。

closure013

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 所構成

closure003

Encapsulation

let obj = {
  name: 'Sam',
  sayHello: function() {
    return `Hello ${this.name}`;
  },
};

obj.sayHello(); // ?

使用 object literal 建立 object 時所有的 property 都是 public,完全沒有任何 encapsulation 可言,該如何實現如 OOP 有 private field 呢 ?

closure014

let obj = (function(name) {
  return {
    sayHello: () => `Hello ${name}`,
  }
}('Sam'));

obj.sayHello(); // ?

使用 Closure + IIFE,name 可如 OOP 的 constructor 般傳入設定初始值,重點是 name 被封裝在內部,外界無法更改,sayHello() 可透過 lexical scope 讀取 name,並藉由 closure 鎖住 name

closure005

let obj = (name => ({ sayHello: () => `Hello ${name}`}))('Sam');

obj.sayHello(); // ?

亦可使用 ES6 的 arrow function 實現。

closure015

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。

closure004

let obj = (name => {
  let addTitle = name => `Mr. ${name}`;
  let title = addTitle(name);

  return {
    sayHello: () => `Hello ${title}`,
  }
})('Sam');

obj.sayHello(); // ?

亦可使用 ES6 的 arrow function 實現。

closure016

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 無值才運算。

closure006

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 不會暴露在外被修改。

closure007

Conclusion

  • Closure 是 FP 的基礎,如此才能回傳新的 function,並以新的 function scope 保留原本的 lexical scope
  • 透過 closure + IIFE,也能以 function 實現 OOP 的 encapsulation

Reference

MDN, Closures