理解閉包

歡迎移步個人博客閱讀:《理解閉包》javascript

閉包 是指能夠包含自由(未綁定到特定對象)變量的代碼塊;這些變量不是在這個代碼塊內或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義(局部變量)。「閉包」 一詞來源於如下二者的結合:要執行的代碼塊(因爲自由變量被包含在代碼塊中,這些自由變量以及它們引用的對象沒有被釋放)和爲自由變量提供綁定的計算環境(做用域)。html

做用域

閉包的一個重點在於做用域,在 JavaScript 中變量的做用域分兩種:全局變量與局部變量,首先讓咱們來了解一下:java

var _global = 1;  // 全局變量

function print() {
  var _internal = 2;  // 局部變量

  console.log(_global); // 1
  console.log(_internal); // 2
  return _internal;
}

print();
console.log(_global); // 1
console.log(_internal); // ReferenceError: _internal is not defined

此時咱們能夠看到,在函數內部是能夠直接讀取全局變量的。但當咱們在外部想訪問內部變量時,就會報錯,由於在函數體外部時沒法訪問函數內部的變量的。es6

須要注意的是,當在函數內部定義變量時沒用使用 var 等聲明變量,那麼它實際上會成爲一個全局變量閉包

function print() {
  _internal = 2;
}

console.log(_internal); // 2

從內存中解釋,變量的聲明都存在棧中,而在 JavaScript 中存在垃圾回收機制(garbage collection),當一個函數執行完返回以後,它的內存會被自動回收,此時函數內部的變量都會被銷燬。函數

那麼咱們有什麼方法能夠保存這一內存,而且在外部訪問函數內部的變量呢 —— 閉包oop

閉包

在正常狀況下,咱們在外部時沒法修改函數內部變量的值:性能

// 場景 1
function print(x) {
  var _internal = 1;

  console.log(_internal + 1);
}

print(1); // 2
// ...
print(1); // 2

咱們能夠看到,不管 print() 調用多少次,打印的值都是 2_internal 的值都是 1學習

這是由於 JavaScript 中的垃圾回收機制,在屢次調用 print() 時,每一次都須要回收前一次的內存,以後再次申請新內存,所以 _internal 沒法在內存中繼續保存。url

換而言之,在每次調用 print() 時都須要爲其和內部的變量申請新的內存空間,第一次 _internal 的內存地址可能爲 0x...1,在函數調用完成以後,這塊內存將被釋放,再次調用時 _internal 的內存地址可能就是 0x...2 了。所以它沒法再內存中被保存下來。

那麼咱們須要在外部使用函數內部的變量時,就須要在函數內部再聲明一個函數,並將其返回:

function print() {
  var _internal = 1;

  return function log() {
    console.log(_internal);
  }
}

var test = print();
test(); // 1

此時,咱們已經能夠從外部訪問 print() 函數內部的變量了。

當咱們須要對 print() 函數內部的 _internal 的值進行修改時,咱們能夠給它另一個函數:

// 場景 2
var add;
function print() {
  var _internal = 1;

  add = function(x) {
    _internal += x;
  }

  return function log() {
    console.log(_internal);
  }
}

var test = print();
test(); // 1
add(1);
test(); // 2

通過上述能夠看出,函數 print() 在通過 add() 運行以後,_internal 的值分別爲 12,這就說明了 _internal 始終保存在內存中,並無在 var test = print(); 調用時被回收。

這是由於 print() 內的 log() 做爲返回值,被賦給 test 這個全局變量,所以 log() 始終在內存中。而 log() 依賴 print() 而且能夠訪問 _internal,因此 print() 也始終在內存中,並且在 var test = print(); 調用時沒有被回收。

換而言之,當 _internal 在聲明的時候分配了內存,咱們能夠將其內存地址表示爲 0x...1,在 print() 函數被調用以後應該會被回收,可是因爲上述緣由,沒有被回收,它的值將繼續保留在地址爲 0x...1 中。在外部可使用指針去尋址,並取得其值。

其餘例子

在循環體中,咱們可能遇到:

function loopA() {
  var arr = [];

  for(var i = 0; i < 10; i++) {
    arr[i] = function() {
      return i;
    }
  }

  return arr;
}

var test = loopA();
test[0]();  // 10
test[1]();  // 10
// ...
test[9]();  // 10

在上述例子中,咱們須要他們執行不一樣的參數獲得不一樣的值。可是一共建立了 10 次匿名函數,,他們都是共享同一個環境的。在匿名函數執行以前,循環早已完成,此時的匿名函數一局指向循環體中的最後一個值了。

  • 解決方案 1:
    es6 中咱們可使用 let 聲明:

    function loopA() {
      var arr = [];
    
      for(let i = 0; i < 10; i++) {
        arr[i] = function() {
          return i;
        }
      }
    
      return arr;
    }
    
    var test = loopA();
    test[0]();  // 0
    test[1]();  // 1
    // ...
    test[9]();  // 9
  • 解決方案 2:
    將函數聲明放在循環體外部:

    function loopA() {
      var arr = [];
      var func = function(n) {
        return n;
      }
    
      for(var i = 0; i < 10; i++) {
        arr[i] = func(i)
      }
    
      return arr;
    }
    
    var test = loopA();
    test[0];  // 0
    test[1];  // 1
    test[9];  // 9
  • 解決方案 3:

    function loopA() {
      var arr = [];
    
      for(var i = 0; i < 10; i++) {
        arr[i] = (function(i) {
          return i;
        })(i)
      }
    
      return arr;
    }
    
    var test = loopA();
    test[0];  // 0
    test[1];  // 1
    test[9];  // 9
  • 其餘解決方案請看參考

弊端

  • 內存泄漏:因爲閉包會使得函數內部的變量都被保存在內存中,不會被銷燬,內存消耗很大。所以須要在退出函數以前,將不使用的變量都刪除。

  • 會修改函數內部變量的值。

總結

閉包是一種特殊的對象。它由兩部分構成:函數,以及建立該函數的環境。環境由閉包建立時在做用域中的任何局部變量組成。
若是不是由於某些特殊任務而須要閉包,在沒有必要的狀況下,在其它函數中建立函數是不明智的,由於閉包對腳本性能具備負面影響,包括處理速度和內存消耗。

參考

百度百科 - 閉包
Wikipedia - Closure
學習 Javascript 閉包(Closure)
MDN - 閉包
深刻理解閉包系列第二篇——從執行環境角度看閉包
深刻理解閉包系列第四篇——常見的一個循環和閉包的錯誤詳解
深刻理解javascript原型和閉包(15)——閉包

相關文章
相關標籤/搜索