閉包的作用域鏈
閉包是有權(quán)訪(fǎng)問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù),比如:
function createFunc(words) {
return function() {
return words;
}
}
var func = createFunc("Hello World!")
func();
// "Hello World!"
上述例子中createFunc的返回值是一個(gè)函數(shù)(閉包),這個(gè)返回值在調(diào)用時(shí)仍然可以訪(fǎng)問(wèn)createFunc的words屬性,這是為什么呢?還記得在之前的文章Javascript 變量、作用域和內(nèi)存問(wèn)題中提到的,一個(gè)函數(shù)在創(chuàng)建時(shí),會(huì)生成一個(gè)內(nèi)部屬性[[scope]],這個(gè)屬性包含函數(shù)被創(chuàng)建的作用域中對(duì)象的集合,也就包括了createFunc的活動(dòng)對(duì)象,而如果沒(méi)有閉包,createFunc的活動(dòng)對(duì)象在調(diào)用結(jié)束時(shí)就可以進(jìn)入GC序列,只有銷(xiāo)毀對(duì)閉包的引用,即func = null,才會(huì)使createFunc的活動(dòng)對(duì)象被GC。
總結(jié)閉包的作用域鏈如下圖:

通過(guò)上述分析我們還可以看出,閉包有一個(gè)不同于普通函數(shù)的特性,就是它會(huì)攜帶包含它的函數(shù)的作用域,因此會(huì)占用更多內(nèi)存。
閉包與變量
閉包的作用域鏈決定了閉包只能包含外部函數(shù)中任何變量的最終值,舉個(gè)例子:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
var result = createFunctions();
result[3](); // 10
上述例子中,我們?cè)?code>createFunctions函數(shù)中,通過(guò)for循環(huán),創(chuàng)建了多個(gè)函數(shù),并期望每一個(gè)都能返回創(chuàng)建它時(shí)的索引值,但結(jié)果發(fā)現(xiàn),每一個(gè)函數(shù)都只能返回i的最終值(由于ECMAScript中沒(méi)有塊級(jí)作用域,因此i是createFunctions中的局部變量),之所以是這樣的結(jié)果,是因?yàn)槊恳粋€(gè)閉包能夠訪(fǎng)問(wèn)到的i都是對(duì)局部變量i的引用,由于這種情況的存在,因此我們發(fā)現(xiàn)很多閉包都是這樣寫(xiě)的:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function() {
return num;
}
}(i);
}
return result;
}
var result = createFunctions();
result[3](); // 3
原理就是將動(dòng)態(tài)的局部變量參數(shù)化,這樣每一個(gè)閉包都保存了該局部變量某個(gè)時(shí)刻的副本。
在閉包中使用this
this是基于執(zhí)行環(huán)境綁定的,如果是全局函數(shù),那么this指window,如果是某個(gè)對(duì)象的函數(shù),那么this指這個(gè)對(duì)象,而匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此閉包中的this一般指window,比如:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
console.log(object.getNameFunc()()); //"The Window"
上述例子中,閉包是在全局環(huán)境中執(zhí)行的,而我們知道,每個(gè)函數(shù)在執(zhí)行時(shí)會(huì)基于執(zhí)行環(huán)境自動(dòng)獲得this,因此this指向了window。
上述例子中,如何使閉包可以訪(fǎng)問(wèn)object呢,可以做如下修改:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
object.getNameFunc()()
// "My Object"
內(nèi)存泄漏
在之前的文章Javascript 變量、作用域和內(nèi)存問(wèn)題提到過(guò)IE在版本9之前,ECMAScript對(duì)象和DOM對(duì)象的GC機(jī)制不同,循環(huán)引用會(huì)導(dǎo)致DOM對(duì)象永遠(yuǎn)不能被回收,學(xué)習(xí)完這一章節(jié)后才發(fā)現(xiàn)自己經(jīng)常寫(xiě)的一段代碼就存在這樣的問(wèn)題!-_-,書(shū)中也提到了這個(gè)例子:
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
console.log(element.id);
};
}
上面的代碼就創(chuàng)建了一個(gè)閉包作為element的事件處理程序,這里的循環(huán)引用體現(xiàn)在element的屬性onclick的值中存在對(duì)element的引用,即使退出assignHandler,element這個(gè)DOM對(duì)象也不會(huì)被引用計(jì)數(shù)機(jī)制GC,那么不在閉包中顯式地引用element,總可以了吧,就比如:
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
console.log(id);
};
}
答案是不行的,因?yàn)殚]包中無(wú)論如何都是要保存一份對(duì) assignHandler活動(dòng)對(duì)象的引用的,自然包含element。
之前提到過(guò),由于閉包中保存的只是函數(shù)活動(dòng)對(duì)象的引用,那么閉包中能夠訪(fǎng)問(wèn)的變量就具有動(dòng)態(tài)性,上個(gè)例子中,閉包由于引用了assignHandler的活動(dòng)對(duì)象,就引用了element,而element引用了一個(gè)DOM對(duì)象,那么,如果element不引用DOM對(duì)象,而是其他對(duì)象,比如null,那么element就可以被標(biāo)記清楚機(jī)制GC。
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
console.log(id);
};
element = null;
}