JavaScript閉包

老毛發表於2019-02-16
本文將帶你用正確姿勢看待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閉包》

相關文章