本文將帶你用正確姿勢看待JavaScript閉包。
在 JavaScript 中閉包描述的是 function 中 外層作用域的變數 被內層作用域 引用的場景,閉包的結構為 內層作用域 儲存了 外層作用域的變數。
要理解閉包,首先要知道 JS詞法作用域 是如何工作的。
JS詞法作用域(lexical scoping)
來看這段程式碼:
let name = 'John';
function greeting() {
let message = 'Hi';
console.log(message + ' '+ name);
}
變數 name 是全域性變數。它可以在任何地方呼叫,包括在 greeting 函式內部。
變數 message 是區域性變數,只能在 greeting 函式內部呼叫。
如果你嘗試從 greeting() 外部訪問 message 變數,會丟擲一個錯誤:
ReferenceError: message is not defined
比較有意思的是 函式內部的作用域是可以巢狀的,如下:
function greeting() {
let message = 'Hi';
function sayHi() {
console.log(message);
}
sayHi();
}
greeting();
// Hi
greeting() 函式 建立了一個區域性變數 message 和一個區域性函式 sayHi()。
sayHi() 是 greeting() 的一個內部方法,只能在 greeting() 內部訪問。sayHi() 可以訪問 greeting() 的 message 變數。在 greeting() 內部呼叫了 sayHi(),列印出了變數 message 的值。
JavaScript閉包(closures)
來修改一下greeting:
function greeting() {
let message = 'Hi';
function sayHi() {
console.log(message);
}
return sayHi;
}
let hi = greeting();
hi(); // 仍然可以獲取到message的值
這次我們不是在 greeting() 執行 sayHi(),而是在 greeting() 被呼叫時把 sayHi 作為結果返回。
在 greeting() 函式外部,宣告瞭一個變數 hi,它是 sayHi() 函式的索引。
這時,我們通過這個索引來執行 sayHi() 函式,可以得到和之前一樣的結果。
通常情況下,一個區域性變數只會在函式執行的時候存在,函式執行完成,會被垃圾回收機制回收。
有意思的是,上邊的這種寫法當我們執行 hi(),message 變數是會一直存在的。這就是閉包的作用,換句話說上面的這種形式就是閉包。
其他示例
下面的例子闡述了閉包更加實用的情況:
function greeting(message) {
return function(name){
return message + ' ' + name;
}
}
let sayHi = greeting('Hi');
let sayHello = greeting('Hello');
console.log(sayHi('John')); // Hi John
console.log(sayHello('John')); // Hello John
greeting() 接收一個引數(message),返回了一個函式接收 一個引數(name)。
greeting 返回的匿名函式 把 message 和 name 做了拼接。
這時 greeting() 表現的行為像 工廠模式。使用它建立了 sayHi() 和 sayHello() 函式,它們都維護了各自的 message ”Hi“ 和 ”Hello“。
sayHi() 和 sayHello() 都是閉包。它們共用了同一個函式體,但是儲存了不同的作用域。
防抖和節流
在面試的時候,經常會有面試官讓你手寫一個防抖,節流函式,其實用到的就是閉包。
如果有興趣可以 檢視一下這篇文章 《防抖和節流例項講解》
好處和問題
閉包的優勢
閉包可以在自己的作用域儲存變數的狀態,不會汙染全域性變數。因為如果有很多開發者開發同一個專案,可能會導致全域性變數的衝突。
閉包可能導致的問題
閉包的優勢可能會成為嚴重的問題,因為閉包中的變數無法被GC回收,尤其是在迴圈中使用閉包:
function outer() {
const potentiallyHugeArray = [];
return function inner() {
potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
console.log('Hello');
};
};
const sayHello = outer(); // contains definition of the function inner
function repeat(fn, num) {
for (let i = 0; i < num; i++){
fn();
}
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray
// now imagine repeat(sayHello, 100000)
在這個例子中,potentiallyHugeArray 會隨著迴圈的次數增加而無限增大而導致記憶體洩漏(Memory Leaks)。
總結
閉包既有優勢,也會導致問題。只有理解了它的原理,才能讓它發揮正確的作用。