閉包:是指有權通路另一個函數作用域中的變量的函數。建立閉包的常見形式:就是在一個函數内部建立另一個函數。
詞法作用域
function outer(){
var localVal = 30;
function inner(){
return localVal;
};
inner()
}
outer();
詞法(lexical)一詞指的是,詞法作用域根據源代碼中聲明變量的位置來确定該變量在何處可用。嵌套函數可通路聲明于它們外部作用域的變量。
閉包
function outer(){
var localVal = 30;
function inner(){
return localVal
};
return inner;
}
var func = outer();
func();
運作這段代碼的效果和之前
outer()
函數的示例完全一樣。其中不同的地方(也是有意思的地方)在于内部函數
inner()
在執行前,從外部函數傳回。
可以看到在JavaScript中的函數形成了閉包。閉包是由函數以及聲明該函數的詞法環境組合而成的。該環境包含了這個閉包建立時作用域内的任何局部變量。在本例子中,func 是執行 outer 時建立的 inner 函數執行個體的引用。inner 的執行個體維持了一個對它的詞法環境(變量 localVal 存在于其中)的引用。是以,當 func 被調用時,變量 localVal 仍然可用,其值 name 就被傳回了。
閉包的作用
1.閉包可以封裝這個函數内的私有變量:
function create_counter(initial) {
var x = initial || 0;
return {
inc: function () {
x += 1;
return x;
}
}
}
var c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3
var c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13
在傳回的對象中,實作了一個閉包,該閉包攜帶了局部變量
x
,并且,從外部代碼根本無法通路到變量
x
。換句話說,閉包就是攜帶狀态的函數,并且它的狀态可以完全對外隐藏起來。
2.閉包還可以把多參數的函數變成單參數的函數。
例如,要計算xy可以用
Math.pow(x, y)
函數,不過考慮到經常計算x2或x3,我們可以利用閉包建立新的函數
pow2
和
pow3
:
function make_pow(n) {
return function (x) {
return Math.pow(x, n);
}
}
// 建立兩個新函數:
var pow2 = make_pow(2);
var pow3 = make_pow(3);
console.log(pow2(5)); // 25
console.log(pow3(7)); // 343
從本質上講,make_pow 是一個函數工廠 — 他建立了将指定的值和它的參數相加求和的函數。
循環錯誤
閉包隻能取得包含函數中任何變量的最後一個值。
下面是一個使用閉包的示例:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
傳回結果:
f1(); // 16
f2(); // 16
f3(); // 16
原因就在于傳回的函數引用了變量
i
,但它并非立刻執行。等到3個函數都傳回時,它們所引用的變量
i
已經變成了
4
,是以最終結果為
16
。
修改的方法一:
需要在函數内部添加立即執行函數,否則每次循環都會在循環的最後一位:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
這個方法是再建立一個函數,用該函數的參數綁定循環變量目前的值,無論該循環變量後續如何更改,已綁定到函數參數的值不變。
修改的方法二:
直接将for循環内的聲明變量i的 var變為 let。
因為var在for循環内部聲明的值,在外部可以擷取到。而let會使for循環變成一個封閉的作用域。
閉包的缺點
如果不是某些特定任務需要使用閉包,在其它函數中建立函數是不明智的,因為閉包在處理速度和記憶體消耗方面對腳本性能具有負面影響。
1.性能考量
例如,在建立新的對象或者類時,方法通常應該關聯于對象的原型,而不是定義到對象的構造器中。原因是這将導緻每次構造器被調用時,方法都會被重新指派一次(也就是說,對于每個對象的建立,方法都會被重新指派)。
考慮以下示例:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
在上面的代碼中,我們并沒有利用到閉包的好處,是以可以避免使用閉包。修改成如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
但我們不建議重新定義原型。可改成如下例子:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
在前面的兩個示例中,繼承的原型可以為所有對象共享,不必在每一次建立對象時定義方法。
2.this指向
this對象是在運作時基于函數函數的執行環境綁定的,在全局函數中,this等于window,而當函數被當作某個對象的方法調用時,this等于那個對象。但是匿名函數具有全局性,是以其this對象通常指向window。
例如:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function(){
return function(){
return this.name;
}
}
}
alert(object.getNameFunc()()); //The Window
這裡沒擷取到這個對象的name,而是拿到了全局的name。因為每個函數在被調用時會自動取得兩個特殊變量:this和arguments。内部函數在搜尋這兩個變量時,隻會搜尋到其活動對象為止,是以永遠不可能直接通路外部函數中的這兩個變量。
把外部作用域的this對象儲存在一個閉包能夠通路到的變量裡,就可以讓閉包通路該對象了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function(){
var that = this;
return function(){
return that.name;
}
}
}
alert(object.getNameFunc()()); //My Object
3.記憶體洩漏
閉包會引用包含函數的整個變量對象,如果閉包的作用域鍊中儲存着一個HTML元素,那麼就意味着該元素無法被銷毀。是以我們有必要在對這個元素操作完之後主動銷毀。
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
}
element = null;
}
閉包會引用包含函數的整個活動對象,其中包含element,包含函數的活動對象中也包含着一個引用,是以需要把element設定為null。這樣可以解除對DOM對象的引用,減少引用次數,確定正常回收其占用的記憶體。
參考:高階函數 閉包
MDN中級教程 Closures