918 字
5 分鐘
JavaScript - Closure(閉包)

什麼是閉包?#

通常情況下,function內部的變數在執行完後就會被釋放,但閉包阻止了這件事,閉包模式可以讓變數駐留在記憶體中不被回收,這是普通的函數不具備的。

NOTE

相關定義:

  • 避免變數全局汙染。
  • 數據私有化,外部無法修改。
  • 可以讓外部使用內部的私有數據。

C1 普通的function寫法:

let a = 10;
function fn(){
a++;
console.log(a);
}
fn();
fn();
fn();
// output = 11、12、13

C2 變數寫在function內的寫法:

let a = 10;
function fn(){
let a = 20;
a++;
console.log(a);
}
fn();
fn();
fn();
o

嗯? 第二個的範例不也是一般的函數嗎? 為什麼輸出結果是文章內所提及的閉包模式特性?

沒錯,避免變數的全局汙染以及數據私有化這完全就是function在做的事情,以及讓外部使用內部的私有數據這也只是return返回數據的操作。到這個階段必須反思,如果僅僅是因為這些需求,根本就不需要用到閉包,平時使用函數已經是在做相同的作業。

什麼樣的情況才是閉包需求?#

先回顧閉包的核心作用讓變數可以駐留在記憶體中,不被回收,接續用C2的範例修改成閉包模式,讓目前的變數a可以像普通函數一樣遞增下去變成21、22、23,再拿一個變數把這個回傳的函數接住。

let a = 10;
function fn(){
let a = 20;
return function(){ // 在這個函數裡面嵌套一個新的函數並回傳(匿名函數也可以)
a++;
console.log(a);
}
}
let f = fn(); // 保存回傳的內部函數,使其形成閉包並記住當時的作用域變數

然後打開瀏覽器的開發者工具(F12/右鍵->檢查)在宣告變數位置增加斷點(Breakpoint)並刷新來源。 ttt

能看見作用域(Scopes)中有Clourse出現,現在裡面有這個變數存在,它的初始值是20,正式完成了閉包的作業,最後輸出看看結果是否有達到預期的遞增。

let a = 10;
function fn(){
let a = 20;
return function(){
a++;
console.log(a);
}
}
let f = fn();
f();
f();
f();
o

輸出結果有正確遞增的話,代表現在這個閉包有正確駐留變數在記憶體中,但如果是像普通的函數一樣執行完後並正常釋放,那麼這就不是閉包,它理應要駐留在記憶體中沒有被回收走才是正確的閉包模式。

IMPORTANT

嵌套的function必須使用到外層函數變數,如果內層沒有使用到外層的函數變數,那麼就不會形成閉包。

閉包會有memory leak嗎?#

既然我們認識了閉包的核心功能,這是否代表會有memory leak(記憶體洩漏)的問題? 沒錯哦,按照C2的範例,現在變數a已經是記憶體洩漏的狀態,數據長時間留在記憶體內沒有被回收釋放,解決方法也很簡單,只需要把f函數結果指向null讓它不再引用就可以被回收走。

let a = 10;
function fn(){
let a = 20;
return function(){
a++;
console.log(a);
}
}
let f = fn();
f();
f();
f();
f = null;

不過有些記憶體洩漏情況是允許的,舉例初始化階段常駐在全域的快取或設定資料…等,甚至是刻意常駐在記憶體當中,這個視專案狀況而定。記憶體就是拿來佔用的,不是不能用多,是不能失控。