本文將帶你用正確姿勢看待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)。
總結
閉包既有優勢,也會導致問題。只有理解了它的原理,才能讓它發揮正確的作用。
文章首發於 IICOOM-個人博客 《JavaScript閉包》