首先咱們看一個最簡單的例子:
javascript
var x = 100;
function fn(){
console.log(x);
}
fn(); // 100複製代碼
毫無疑問,"fn()" 函數是能夠訪問到外部定義的變量 "x"。函數被建立時,都會建立其"做用域"。"fn" 函數被建立,其做用域內未聲明變量 "x" ,只能到上一級的做用域(這裏是全局做用域)找,這裏的 "x" 稱之爲 "自由變量" 。html
function fn() {
var x = 10;
return function (n) {
return x > n ? x : n
}
}
let moreThanTen = fn()
console.log( moreThanTen(9) )
console.log( moreThanTen(11) )複製代碼
上述例子中,"fn" 內部建立了變量 "x" 和返回一個匿名函數,匿名函數使用到父級的自由變量 "x" 。java
正常來講,第七行執行 "fn" 函數以後,其函數做用域應該銷燬,但匿名函數須要用到父級變量,於是產生了閉包。web
閉包是由函數以及建立該函數的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的全部局部變量。編程
var Counter = (function() {
// 私有變量
var privateCounter = 0;
// 私有方法,用於修改私有變量
function changeBy(val) {
privateCounter += val;
}
return {
increment: function(n) {
changeBy(n);
},
decrement: function(n) {
changeBy(-n);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); // 0
Counter.increment(3);
console.log(Counter.value()); // 3
Counter.decrement(1);
console.log(Counter.value()); // 2複製代碼
這個環境中包含兩個私有項:名爲 privateCounter 的變量和名爲 changeBy 的函數。這兩項都沒法在這個匿名函數外部直接訪問。必須經過匿名函數返回的三個公共函數訪問。這三個公共函數是共享同一個環境的閉包。bash
以這種方式使用閉包,提供了許多與面向對象編程相關的好處 —— 特別是數據隱藏和封裝。閉包
首先看一個官方錯誤例子:app
<p id="help">Helpful notes will appear here</p> <p>E-mail: <input type="text" id="email" name="email"></p> <p>Name: <input type="text" id="name" name="name"></p> <p>Age: <input type="text" id="age" name="age"></p>複製代碼
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
// 問題所在
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();複製代碼
運行這段代碼後,您會發現它沒有達到想要的效果。不管焦點在哪一個input上,顯示的都是關於年齡的信息。函數
緣由是賦值給 onfocus 的是閉包。這些閉包是由他們的函數定義和在 setupHelp 做用域中捕獲的環境所組成的。這三個閉包在循環中被建立,但他們共享了同一個詞法做用域,在這個做用域中存在一個變量item。當onfocus的回調執行時,item.help的值被決定。因爲循環在事件觸發以前早已執行完畢,變量對象item(被三個閉包所共享)已經指向了helpText的最後一項。能夠理解爲,三個 onfocus 函數共享一個閉包環境。性能
最爲簡單的解決辦法是把循環中的 "var" 關鍵字改成 "let"。以下:
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}複製代碼
或者使用匿名閉包:
for (var i = 0; i < helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // 立刻把當前循環項的item與事件回調相關聯起來
}複製代碼
若是不是某些特定任務須要使用閉包,在其它函數中建立函數是不明智的,由於閉包在處理速度和內存消耗方面對腳本性能具備負面影響。
例如,在建立新的對象或者類時,方法一般應該關聯於對象的原型,而不是定義到對象的構造器中。緣由是這將致使每次構造器被調用時,方法都會被從新賦值一次(也就是,每一個對象的建立)。
function fnVar() {
for (var i = 0; i < 5; i++) { }
console.log(i)
}
function fnLet() {
for (let i = 0; i < 5; i++) { }
console.log(i)
}
fnVar() // 5
// fnLet() // 會報錯 複製代碼
在ES6 以前,JavaScript 是沒有塊級做用域,上述例子證實了 "var" 聲明的變量在塊做用域仍然存在。
ES6 中新增的 "let" 關鍵字提供了塊級做用域,其聲明的變量在塊外不能訪問。
附:MDN的閉包講解。