點燈坊

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

ECMAScript 之 IIFE

Sam Xiao's Avatar 2020-11-14

IIFE 為 FP 的招牌菜,由於其獨特的 Lexical Scope,使其在 ECMAScript 有不少獨特應用。

Version

ECMAScript 2015

IIFE

let f = x => x + 1

f(2)

傳統我們會先定義 function,然後呼叫執行之。

對於這種只使用一次的 function,反正最後都是要執行,可使用 IIFE 進行化簡。

(x => x + 1)(2) // ?

x => x + 1 為 anonymous function,但因為 function 沒有名稱,該如何執行呢 ?

可使用 () 將 arrow function 刮起來,之後再加上 () 傳入 argument 立即執行。

IIFE
全名為 Immediately Invoked Function Expression,提供了執行 anonymous function 方法

iife000

Global Variable

let count = 0

let inc = () => count++

inc() // ?
inc() // ?
inc() // ?

傳統若要實作 counter,會在 global scope 建立 count variable,在 inc() 以 side effect 更新 global variable。

這樣做有兩個缺點:

  • count 為定義在 window object 的 global property
  • inc() 為定義在 window object 的 global method

如此雙雙污染了 global scope 與 window object。

iife005

let inc = (x => {
  let count = x
  return () => count++
})(0)

inc() // ?
inc() // ?
inc() // ?

比較好的是在 IIFE 以回傳 function,如此 count variable 被鎖在 IIFE 內藉由 closure 鎖住 count,不會污染 global scope。

iife006

New Scope

for(var i = 0; i < 5; i++)
  setTimeout(() => console.log(i), 1000)

這也是 ECMAScript 很有名題目,若我們希望在 5 秒之內每秒依序印出 01234

直覺會使用 for loop 搭配 setTimeout()

iife001

但結果為 同時 印出 5,並不如預期。

因為 setTimeout() 為 asynchronous function,並非立即執行,而是先註冊到 callback queue,等 synchronous function 都執行完,才再 1000ms 後同時執行,因此時間該改成 1000 * i 才對。

另外 var 所宣告 variable 為 function scope,當 _ => console.log(i) 要執行時,i 已經累計到 5,因此我們要對 setTimeout() 建立自己的 scope 能鎖住 i

for(var i = 0; i < 5; i++) {
  (i => {
    setTimeout(() => console.log(i), 1000 * i)
  })(i)
}

ES5 的標準解法就是使用 IIFE 為 setTimeout() 建立新的 scope 鎖住 i,因此建立 parameter 為 i 的 IIFE 將 for loop 的 i 傳入。

iife002

for(let i = 0; i < 5; i++)
  setTimeout(() => console.log(i), 1000 * i)

ES6 則有更簡單解法,由於 let 是以 {} 的 block scope,因此 for 的 body {} 就自帶 scope,可不必使用 IIFE。

iife003

Alias

let f = (x, y) => {
  let z = x + y

  return z * 2 + 1
}

f(2, 3) // ?

實務上常在 function 內根據 parameter 建立新的 variable。

let f = (x, y) => (z => z * 2 + 1)(x + y)

f(2, 3) // ?

可建立 arrow function 以 parameter 取代 variable,定義則改用 IIFE 以 argument 傳入。

iife004

Lexical Scope

((w, d) => {
  
})(window, document)

有時候會看將 windowdocument 這類 global object 以 IIFE 的 argument 傳入,其目的有二:

  • 在 arrow function 內只要以 wd 即可,程式碼較精簡
  • 由於 arrow function 就可找到 w 代表 windowd 代表 document,因此查找 lexical scope 時速度較快

Conclusion

  • 隨著 ECMAScript 的演進,IIFE 的重要性不若以往,ES module 可取代 IIFE 的 避免污染 global scope;let 可取代以 IIFE 建立 scope;SPA 與 data binding 出現也很少直接存取 windowdocument,唯藉由 IIFE 的 parameter 實現 alias 或在 IIFE 回傳 function 以 closure 鎖住 variable 在實務上還常常用到

Reference

John Resig, Secrets of the JavaScript Ninja
許國政, 008 天重新認識 JavaScript
Dr. Axel Rauschmayer, Speaking JavaScript