Jun

只要我的心還會跳,腿還能動

我就沒有理由停下前進的步伐

淺談 Closure 閉包


變數的作用域

要理解閉包必須先理解 JS 的變數作用域,在 JS 裡變數的作用域分為全域變數與函數內部的變數。

JS 特別的地方在於函數內部可以讀取全域變數:

var num = 99;

function func() {
  console.log(num); // 99
}

func();

反之,在函數外部則無法讀取函數內部的變數:

function func() {
  var num = 99;
}

console.log(num); // num is not defined

但需要注意一點,函數內部在宣告變數的時候,記得要使用 var 來宣告,如果不用的話,實際上是宣告了一個全域變數:

function func() {
  num = 99;
}

func();

console.log(num); // 99

如何從外部讀取函數內部的變數?

那就是在函數的內部,再宣告一個函數:

function func1() {
  var num = 99;

  function func2() {
    console.log(num);
  }

  return func2;
}

var res = func1();

res(); // 99

上面是一個非常簡單及經典的閉包範例,函數 func2 存在於函數 func1 之中,這時函數 func1 內部的變數對於函數 func2 而言都是可見的,但反之,則不可見,函數 func2 內部的變數對於函數 func1 而言就是不可見的,這是 JS 詞彙環境機制下的結果,函數實際上被寫在何處會左右 JS 查找變數時的方向。

所以說既然 func2 可以讀取 func1 內部的變數,那麼我們只要把 func2 回傳,我們就可以在 func1 之外讀取其內部的變數了!


閉包的概念

粗略地來講,閉包就是能夠讀取其它函數內部變數的函數,由於只有函數內部的函數才能讀取函數內部的變數,因此也可以把閉包簡單的理解為定義在一個函數內部的函數。

但我會更偏向於形容閉包就是將函數內部和函數外部鏈接起來的一座橋梁。


閉包的用途

關於第二點的範例:

function func1() {
  var num = 99;

  plusOne = function () {
    num += 1;
  };

  function func2() {
    console.log(num);
  }

  return func2;
}

var res = func1();

res(); // 99
plusOne();
res(); // 100
plusOne();
res(); // 101

在上述的範例中,res 實際上就是存儲了一個閉包,res 執行了三次,結果分別為 99,100,101 這證明了函數 func1 內部的變數 num 始終保存於內存中沒有釋放,這是就是一個閉包最明顯的特徵。

為什麼會這樣呢?

原因在於 func1func2 的父函數,而 func2 被賦值給了一個全域變數 res,這導致 func2 始終存在於內存中,然而 func2 的存在依賴於 func1,因此函數 func1 也始終存在於內存中,不會因為在調用結束後就被垃圾回收機制給回收了。