要知道閉包是什麼之前,要先了解範圍鏈 (Scope Chain)
範圍鏈 Scope Chain
在 ES6 之前只能用 var ,創造出函式作用域,在函式內用 var 宣告變數的話,該變數的作用域的範圍就只在這個的函式,外層取不到函式內變數的值,我們可以記得一個重點「切分變數的有效範圍的最小單位是函式」。
而 let 和 const 的作用域 (區塊作用域) 則是 { } 在大括號內宣告變數,在大括號外就取不到變數的值
函式內的函式
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所宣告的變數youter外層函式取不到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()中,可以達到兩件事情 - count 不會暴露在 global 環境造成變數衝突
- 也可確保內部的 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 互相影響 countFunc與countFunc2分別是「獨立」的計數器實體,彼此不會互相干擾,也不需要特別新增其他變數來儲存狀態
閉包常見誤區
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()