[觀念筆記] 什麼是閉包?

更新 發佈閱讀 8 分鐘

要知道閉包是什麼之前,要先了解範圍鏈 (Scope Chain)

範圍鏈 Scope Chain

在 ES6 之前只能用 var ,創造出函式作用域,在函式內用 var 宣告變數的話,該變數的作用域的範圍就只在這個的函式,外層取不到函式內變數的值,我們可以記得一個重點「切分變數的有效範圍的最小單位是函式」。

letconst 的作用域 (區塊作用域) 則是 { } 在大括號內宣告變數,在大括號外就取不到變數的值

函式內的函式

function outer() {
// outer 在這層取不到變數 z
var y = x * 2;

function inner(z) {
console.log(x, y, z); // 1 2 6
}
inner(y * 3);
}

var x = 1; // 在 global 這層只有 x,找不到 y 和 z
outer(x);
  • outer 外層函式裡面有一個 inner 內層函式,而 inner 可以取到外層 outer 所宣告的變數 y
  • outer 外層函式取不到 inner 內層函式變數 z
  • 由此可以發現,若在自己的層級找不到值,就會一層一層的往外找,直到最外層 global,這個稱為「範圍鍊」。

若在函式內沒有用關鍵字 var 來宣告變數,會發生什麼事?

function outer() {
// 把 var 拿掉
y = x * 2;

function inner(z) {
console.log(x, y, z);
}
inner(y * 3);
}

var x = 1; // 在 global 這層只有 x,找不到 y 和 z
outer(x);
  • y 會變成「全域變數」,也就是 window 下的一個屬性
  • outer 沒有宣告變數,由於範圍鏈的關係,會一層一層的往外找這個變數的定義,於是在 global 生成一個變數

閉包 Closure

  • MDN 對閉包的定義:
    閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合。
  • 白話文解釋:
    假設現在有兩個函式,一個是外部函式,另一個是定義在外部函式內的內部函式。因為 scope chain 的關係,這個內部函式可以取到外部函式裡宣告的變數。 一般情況下,當一個函式執行完畢,函式內變數的值的記憶體會被釋放掉,但因為閉包的寫法,內部函式持續保有對外部函式變數的參照(有用到外部函式的變數),導致這個變數不會被釋放,外部函式的變數也就因此被保留在記憶體中,形成一個閉包的作用域環境。

閉包範例

其實前面的範例就是閉包的寫法,不過還是來看一下其他範例:

function counter(){
var count = 0;

function innerCounter(){
return ++count; // 回傳 count + 1 後結果
}

return innerCounter; // 將函式回傳
}

var countFunc = counter();

console.log( countFunc() ); // 1
console.log( countFunc() ); // 2
console.log( countFunc() ); // 3
  • 原始寫法可能會將 count 宣告在全域,但如果程式碼變多,就有可能跟其他人定義的變數名稱發生衝突、變數無法回收造成記憶體洩漏 (mermory leak) 的問題
  • 改成像上面閉包的寫法,把 count 封裝在 counter() 中,可以達到兩件事情
    1. count 不會暴露在 global 環境造成變數衝突
    2. 也可確保內部的 count 可以被修改

更簡化寫法:

function counter(){
var count = 0;

return function(){ // 直接回傳匿名函式
return ++count;
}
}

搭配 ES6 的箭頭函式,更簡短

function counter(){
var count = 0;
return () => ++count;
}

透過全域變數,去儲存新的 count 狀態

function counter() {
var count = 0;

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

const countFunc = counter();
const countFunc2 = counter();

console.log(countFunc()); // 1
console.log(countFunc()); // 2
console.log(countFunc()); // 3

console.log(countFunc2()); // 1
console.log(countFunc2()); // 2
  • 改閉包寫法,可以新增全域變數儲存新的 count 狀態,也就是新增獨立的計數器,不怕變數 count 互相影響
  • countFunccountFunc2 分別是「獨立」的計數器實體,彼此不會互相干擾,也不需要特別新增其他變數來儲存狀態

閉包常見誤區

function counter() {
var count = 0;

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

console.log(counter()); // 會回傳內部的函式

console.log(counter()()); // 1
console.log(counter()()); // 1
console.log(counter()()); // 1
  • counter() 會回傳內部的函式
    ƒ () {
    return ++count;
    }
  • counter()() 會建立一個新的 count = 0,函後馬上執行 ++count,因為每個都是全新建立的,回傳結果才都是 1
  • 所以閉包的使用方法會定義一個全域變數,賦值 counter(),而不是直接呼叫 counter()

參考資料:

  1. https://ithelp.ithome.com.tw/articles/10193009
  2. https://blog.huli.tw/2018/12/08/javascript-closure/
  3. https://medium.com/狗奴工程師/閉包包什麼-探索js中的作用域與closure-javascript鍛鍊日記-f7b1a2ac1e2a
  4. https://www.explainthis.io/zh-hant/swe/what-is-closure
  5. https://nissentech.org/why-do-we-need-closure/
留言
avatar-img
Go Go Coding
2會員
4內容數