理解 JavaScript 閉包

這是本系列的第 4 篇文章。前端

做爲 JS 初學者,第一次接觸閉包的概念是由於寫出了相似下面的代碼:segmentfault

for (var i = 0; i < helpText.length; i++) {
  var item = helpText[i];
  document.getElementById(item.id).click = function() {
    showHelp(item.help);
  }
}

給列表項循環添加事件處理程序。當你點擊列表項時不會有任何反應。如何在初學就理解閉包?你須要接着讀下去。閉包

§ 什麼是閉包

說閉包前,你還記得詞法做用域嗎?函數

var num = 0;
function foo() {
  var num = 1;
  function bar() {
    console.log(num);
  }
  bar();
}
foo(); // 1

執行上面的代碼打印出 1。性能

bar 函數是 foo 函數的內部函數,JS 的詞法做用域容許內部函數訪問外部函數的變量。那咱們可不能夠在外部訪問內部函數的變量呢?理論上不容許。學習

可是咱們能夠經過某種方式實現,即將內部函數返回。spa

function increase() {
  let count = 0;
  function add () {
    count += 1;
    return count;
  }
  return add;
}

const addOne = increase();

addOne(); // 1
addOne(); // 2
addOne(); // 3

內部函數容許訪問其父函數的內部變量,那麼將內部函數返回到出來,它依舊引用着其父函數的內部變量。prototype

這裏就產生了閉包。code

簡單來講,能夠把閉包理解爲函數返回函數對象

上面的代碼中,當 increase 函數執行,壓入執行棧,執行完畢返回一個 add 函數的引用,因此 increase 函數內部的變量對象依舊保存在內存中,不會被銷燬。

調用 addOne 函數,至關於執行內部函數 add,它能夠訪問其父函數的內部變量,從而修改變量 count。而調用 addOne 函數所在的環境爲全局做用域,不是定義 add 函數時的函數做用域。

因此,我理解的閉包是一個函數,它在執行時與其定義時所處的詞法做用域不一致,而且具備可以訪問定義時詞法做用域的能力。MDN 這樣定義:閉包是函數和聲明該函數的詞法環境的組合

§ 閉包的利與弊

◆ 利

第一,閉包能夠在函數外部讀取函數內部的變量。

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
})();

Counter.value(); // 0
Counter.increment();
Counter.increment();
Counter.value(); // 2
Counter.decrement();
Counter.value(); / 1

上面這種模式稱爲模塊模式。咱們使用當即執行函數 IIFE 將代碼私有化可是提供了可訪問的接口,經過公共接口來訪問函數私有的函數和變量。

第二,閉包將內部變量始終保存在內存中。

function type(tag) {
  return function (data) {
    return Object.prototype.toString.call(data).toLowerCase() === '[object ' + tag + ']';
  }
}

var isNum = type('number');
var isString = type('string');

isNum(1); // true
isString('abc'); // true

利用閉包將內部變量(參數)tag 保存在內存中,來封裝本身的類型判斷函數。

◆ 弊

第一,既然閉包會將內部變量一直保存在內存中,若是在程序中大量使用閉包,勢必形成內存的泄漏。

$(document).ready(function() {
  var button = document.getElementById('button-1');
  button.onclick = function() {
    console.log('hello');
    return false;
  };
});

在這個例子中,click 事件處理程序就是一個閉包(在這裏是個匿名函數),它將引用着 button 變量;而 button 在這裏自己依舊引用着這個匿名函數。從而產生循環引用,形成網頁的性能問題,在 IE 中可能會內存泄漏。

解決辦法就是手動解除引用。

$(document).ready(function() {
  var button = document.getElementById('button-1');
  button.onclick = function() {
    console.log('hello');
    return false;
  };
  button = null; // 添加這一行代碼來手動解除引用
});

第二,若是你將函數做爲對象使用,將閉包做爲它的方法,應該特別注意不要隨意改動函數的私有屬性。

§ 閉包的經典問題

◆ 循環

如今咱們來解決一下文章開頭出現的問題。

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

for (var i = 0; i < helpText.length; i++) {
  var item = helpText[i];
  document.getElementById(item.id).click = makeHelpCallback(item.help);
}

額外聲明一個 makeHelpCallBack 的函數,將循環每次的上下文環境經過閉包保存起來。

◆ setTimeout

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
};

結果爲 1 秒後,打印 5 個 5。

咱們能夠利用閉包保留詞法做用域的特色,來修改代碼達到目的。

for (var i = 0; i < 5; i++) {
  setTimeout((function(i) {
    return function () {
      console.log(i);
    }
  }(i)), 1000);
};

結果爲 1 秒後,依次打印 0 1 2 3 4。

§ 小結

閉包在 JS 中隨處可見。

閉包是 JS 中的精華部分,理解它須要具有必定的做用域、執行棧的知識。理解它你將收穫巨大,你會在 JS 學習的道路上走得更遠,好比會在後面的文章來討論高階函數和柯里化的問題。

◆ 文章參考

閉包 | MDN

學習 JavaScript 閉包 | 阮一峯

Understanding JavaScript Closures: A practical Approach | Paul Upendo

閉包形成問題泄漏的解決辦法 | CSDN

§ JavaScript 系列文章

理解 JavaScript 執行棧

理解 JavaScript 做用域

理解 JavaScript 數據類型與變量

歡迎關注個人公衆號 cameraee

clipboard.png

前端技術 | 我的成長

相關文章
相關標籤/搜索