這是本系列的第 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 的函數,將循環每次的上下文環境經過閉包保存起來。
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
歡迎關注個人公衆號 cameraee
前端技術 | 我的成長