ES20XX Modern JS 新語言特性筆記 | Closures

2021-08-15

筆記從 ES2015 / ES2016 / ES2017 / ES2018 / ES2019 / ES2020 以來,JS 所加入的新語言特性。使用的情境新語言特性不僅是為了減少對第三方套件的依賴 (Lodash),也是基於 ESLint 上 Clean Code、Best Practices 規範要求。熟悉新語言特性不僅能夠使 JS 開發更加快速也能夠提升閱讀性。

本篇筆記為介紹 JavaScript 令人迷惑的概念 Cloures。

JavaScript logo

說明

Closures 最早是從 Python 或者是 JavaScript 認識已經不太清楚了,但在 JS 的領域討論 Closures 的人似乎較多一些。而 Closures 的中文翻譯「閉包」更是讓人困惑,音有點像細胞,但顯然兩者沒有任何關閉。而在摸索之後,其實關於 Closures 必須要看過應用的範例,再重新思索它的定義,才能夠真正明白為什麼會翻譯為閉包:

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
MDN Web Docs

其實閉包是有關程式語言變數作用域 (Lexical Scope) 的一個程式概念,閉包是將函式以及其所屬環境資訊記憶起來,以提供外部使用。藉由閉包可以達到函式的重複使用,以及在 JS 中以替代的方式設計物件導向設系中的 Private Method。

使用範例

function foo(){
    let count = 0;

    bar = function(){
        return ++count;
    }

    return bar
}
f = foo()
f2 = foo()

f() // 1
f() // 2
f2() // 1
f() // 3
f2() //2

由於 f() 及 f2() 有著不同的環境資訊 count,所以呼叫函式得到的結果不同。

實際應用 | Reuse Function

首先定義函式為 Closure 的設計方式,讓程式碼可以被重複使用。

function exponential(power){
    return function(base){
        return base ** power;
    } 
}

接著可以重複利用程式碼,專門計算平方、三次、四次方的函數:

square = exponential(2)
toTheThirdPower = exponential(3)
toTheFourthPower = exponential(4)

使用方式:

square(3)
// 9

toTheThirdPower(3)
// 27

toTheFourthPower(3)
// 81

實際應用 | Private Method

...

setTimeout 在作用域上的經典問題

for (var index = 1; index <= 3; index++) {
    setTimeout(function () {
        console.log('after ' + index + ' second(s):' + index);
    }, index * 1000);
}

如果使用 var 會每間隔 1 秒回應一次下列訊息

after 4 seconds(s):4
after 4 seconds(s):4
after 4 seconds(s):4

原因在於 for 與 setTimeout 的搭配作用順序如下:

  1. index 設為 1
  2. 呼叫 setTimeout() 執行時間為 1000 ms 後
  3. index 設定 2
  4. 呼叫 setTimeout() 執行時間為 2000 ms 後
  5. index 設定 3
  6. 呼叫 setTimeout() 執行時間為 3000 ms 後
  7. index 設定 4

以上的動作對於 JS Runtime 來說飛快可以執行完成。

而在 for-loop 中的 setTimeout callback function 會共享 global scope 的 index。

  1. 當時間為 1000ms 時,使用 callback function,closure,顯示為 4
  2. 當時間為 2000ms 時,使用 callback function,closure,顯示為 4
  3. 當時間為 3000ms 時,使用 callback function,closure,顯示為 4

改為使用 ES6 的 let 則能夠讓產生新的 lexical scope 於每一個 callback function,因此對於 setTimeout callback function 而言,每一個 index 都是獨立的。

for (let index = 1; index <= 3; index++) {
    setTimeout(function () {
        console.log('after ' + index + ' second(s):' + index);
    }, index * 1000);
}
after 1 seconds(s):1
after 2 seconds(s):2
after 3 seconds(s):3

中所收到的 callback function 會有 closure 的行為記憶處其環境資訊,因此如果是以 var 的方式宣告 index,當 callback 要執行時,此時

The reason you see the same message after 4 seconds is that the callback passed to the setTimeout() a closure. It remembers the value of i from the last iteration of the loop, which is 4.

In addition, all three closures created by the for-loop share the same global scope access the same value of i.

參考資料

JavaScript Tutorial

Closures | MDN

Variable scope, closure | JAVASCRIPT.INFO

Node.js Dancing with ES20XX

查詢 Node.js 各版本對於 Ecma JavaScript 的支援情況。目前主要使用的 Node.js 12.10 對於 ES2019 以前都有相當高的支援情況,所以使用新 Feature 無煩惱。

Node.green