JS的閉包真的是一個老生常談的知識點了狂窑,無奈它并不是那么好掌握,但是它又是那么重要贩耐,很多高級應(yīng)用的開發(fā)都會用到閉包去解決相關(guān)問題耻瑟。希望你能從這篇文章了解基本的閉包知識點旨指,后期會不定期更新使之更加完善捻悯。
在了解閉包知識以前,先說下函數(shù)作用域問題淤毛。在一些類似C語言的編程語言中,它們是具有塊級作用域(block scope)的算柳,但是在JavaScript中卻沒有塊級作用域低淡,取而代之的是函數(shù)作用域(function scope):“變量在聲明它們的函數(shù)體以及這個函數(shù)體嵌套的任意函數(shù)體內(nèi)都是有定義的”。(引用自《JavaScript權(quán)威指南》)
例如如下代碼:
function test(o)?{
var i = 0;????// i在整個函數(shù)體中都是有定義
if(typeof o ==?"object")?{
var j = 0;????// j在函數(shù)體中是有定義的瞬项,不僅僅是在這段代碼內(nèi)
for(var k = 0; k < 10; k++)?{??// k在函數(shù)體內(nèi)是有定義的蔗蹋,不僅僅是在循環(huán)內(nèi)
console.log(k);????//?輸出數(shù)字0-9
}
console.log(k);????// k已經(jīng)定義了,輸出數(shù)字10
}
console.log(j);????// j已經(jīng)定義了囱淋,但是可能沒有初始化
}
上面這段代碼中猪杭,如果我們傳入不同的參數(shù),j的打印結(jié)果是不一樣的妥衣。比如:
var o =?{name:?"qin", old: 23};
test(o);
當(dāng)傳入?yún)?shù)為object時皂吮,會執(zhí)行if判斷語句中的代碼塊,這個時候j的值打印出來為0税手;
var o =?"qin";
test(o);
當(dāng)傳入?yún)?shù)不為object的時候蜂筹,就不會執(zhí)行if判斷語句中的代碼塊,這個時候j的值打印為undefined芦倒。這個時候的變量j就屬于定義了未初始化艺挪。
JavaScript的函數(shù)作用域是指在函數(shù)內(nèi)聲明的所有變量在函數(shù)體內(nèi)始終是可見的。而且JavaScript函數(shù)里聲明的所有變量都會被提前至函數(shù)頂部兵扬,這叫做聲明提前(hoisting)麻裳。
在JavaScript中每一個全局代碼或函數(shù)所包含的代碼塊中,都有一個與之關(guān)聯(lián)的作用域鏈(scope chain)器钟,只有弄懂這個作用域鏈才能更好的去理解閉包問題津坑。
作用域鏈?zhǔn)且粋€對象列表或者鏈表,當(dāng)執(zhí)行JavaScript代碼需要查找變量y的值的時候(這個過程叫做“變量解析”)傲霸,它會從鏈表中的第一個對象開始查找国瓮,如果這個對象中有一個名為y的屬性,則會直接使用這個值狞谱,如果沒有乃摹,就會繼續(xù)查找下一個對象,以此類推跟衅。當(dāng)整個鏈表的對象中都沒有y這個屬性的話孵睬,就會拋出一個引用異常(ReferenceError)的錯誤。
簡單理解就是:子對象會一級一級地向上尋找所有父對象的變量伶跷。所以掰读,父對象的所有變量秘狞,對子對象都是可見的,反之則不成立蹈集。
那么這個作用域鏈表對象創(chuàng)建規(guī)則是怎樣的呢烁试?大致分三種情況:
1.整個代碼不包含任何函數(shù):
這時的作用域鏈對象是由一個全局對象組成。比如:
var n =?"qin";
if(typeof n ==?"string")?{
for(var i = 0; i < 5; i++)?{
console.log(i);
}
}
console.log(i);
上面這段代碼中的作用域鏈為{n:?"qin", i: 5}拢肆。
2.整個代碼包含函數(shù)减响,但不包括嵌套函數(shù):
這時的作用域鏈有兩個對象,第一個是定義函數(shù)參數(shù)和局部變量的對象郭怪,第二個是全局對象支示。比如:
var x =?"qin";
function test()?{
var k = 3;
for(var i = 0; i < 3; i++)?{
k += 1;
}
console.log(k);?????//?打印結(jié)果為6
}
test();
console.log(x);????//?打印結(jié)果為qin
console.log(i);????//?會拋出ReferenceError錯誤
上面這段代碼中存在兩個作用域鏈,第一個是函數(shù)局部變量的作用域鏈{i: 3, k: 6, x:?"qin"}鄙才,第二個是全局對象的作用域鏈{x:?"qin"}颂鸿。當(dāng)我們在函數(shù)外部打印i的值的時候,JavaScript會去全局對象的作用域鏈查找屬性為i的值攒庵,但是全局對象的作用域鏈并不存在i這個屬性嘴纺,因此就會拋出引用異常錯誤(ReferenceError)。
3.代碼中存在函數(shù)浓冒,且有嵌套函數(shù):
這時的作用域鏈至少有三個對象(因嵌套函數(shù)的數(shù)量增加而增加)颖医,第一個是全局對象的作用域鏈,第二個是最外層函數(shù)的參數(shù)和局部變量的作用域鏈裆蒸,第三個是嵌套函數(shù)的參數(shù)和局部變量的作用域鏈熔萧。比如:
var x = 0;
function test()?{
var y = 2;
x += 1;
function foo()?{
var a = 3;
console.log(y);????//?結(jié)果為2
}
foo();
console.log(a);????//?會拋出ReferenceError錯誤
}
test();
console.log(x);????//?結(jié)果為1
上面這段代碼中,函數(shù)test中包含一個嵌套函數(shù)foo,這段代碼含三個作用域鏈僚祷,第一個是全局對象作用域鏈{x: 1}佛致,第二個是函數(shù)test的作用域鏈{y: 2, x: 1},第三個是嵌套函數(shù)foo的作用域鏈{a: 3辙谜,y: 2, x: 1}俺榆。
當(dāng)我們定義一個函數(shù)的時候,其實它就已經(jīng)保存了一個作用域鏈装哆。當(dāng)我們調(diào)用這個函數(shù)罐脊,它會創(chuàng)建新的對象來存儲它的局部變量,并將這個對象添加到保存的那個作用域鏈上蜕琴,同時還會創(chuàng)建一個新的更長的表示函數(shù)調(diào)用作用域的“鏈”萍桌。對于嵌套函數(shù)來說,每次調(diào)用外部函數(shù)的時候凌简,內(nèi)部函數(shù)又會重新定義一遍上炎。因為每次調(diào)用外部函數(shù)的時候,作用域鏈都是不同的雏搂。內(nèi)部函數(shù)在每次定義的時候都會有微妙的差別——在每次調(diào)用外部函數(shù)時藕施,內(nèi)函數(shù)的代碼都是相同的寇损,而且關(guān)聯(lián)這段代碼的作用域鏈也不相同。(引用自《JavaScript權(quán)威指南》)
關(guān)于變量作用域及函數(shù)作用域可參考這篇文章:什么是變量作用域和函數(shù)作用域裳食?(坑未填)
說完函數(shù)作用域和作用域鏈矛市,接下來就要開始理解什么是閉包了。
首先在上面包含嵌套函數(shù)的例子中诲祸,我們?nèi)绾卧谕鈱雍瘮?shù)test中訪問到嵌套函數(shù)foo中的變量a的值呢浊吏?
其實很簡單,我們只需要把foo中a變量返回就可以了烦绳,如下:
var x = 0;
function test()?{
var y = 2;
x += 1;
function foo()?{
var a = 3;
console.log(y);????//?結(jié)果為2
return a;
}
var res = foo();
console.log(res);//?結(jié)果為3
}
test();
console.log(x);????//?結(jié)果為1
其實上面的代碼就是典型的閉包,閉包函數(shù)為foo配紫。
個人理解閉包的概念就是:
有權(quán)訪問另一個函數(shù)作用域內(nèi)變量的函數(shù)就是閉包径密。
本質(zhì)上閉包就是將函數(shù)內(nèi)部與外部聯(lián)系起來的一座橋梁。
閉包的用途:
閉包的最大用處有兩個:
第一是上面所說的可以讀取到函數(shù)內(nèi)部的變量躺孝。
第二則是讓這些變量值能一直保存在內(nèi)存中享扔。
來看一個比較有趣的例子:
function makeAdder(x)?{
return function(y)?{
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2));?????// 7
console.log(add10(2));???// 12
上面的例子中,我們定義了函數(shù)makeAdder植袍,它接受一個參數(shù)x惧眠,并返回一個新的函數(shù)。它返回的函數(shù)使用一個參數(shù)y于个,并返回x+y的值氛魁。
add5和add10都是兩個閉包函數(shù),我們?yōu)樗麄儌魅氩煌膮?shù)x厅篓,一個為5秀存,一個為10。實際上這兩個函數(shù)有著各自獨立的作用域鏈羽氮,并不相互影響或链。
使用閉包應(yīng)該注意的點:
1.由于閉包會使得函數(shù)中的變量都會被保存在內(nèi)存中,內(nèi)存消耗很大档押,因此不能濫用閉包澳盐,否則會導(dǎo)致網(wǎng)頁性能問題,在IE中還可能造成內(nèi)存泄漏令宿。解決辦法是在退出函數(shù)之前叼耙,將不使用的局部變量全部刪除。
2.閉包會在父函數(shù)外部改變父函數(shù)內(nèi)部的值粒没,所以你在把父函數(shù)當(dāng)做對象使用旬蟋,把閉包當(dāng)做它的公用方法,把內(nèi)部變量當(dāng)做它的私有屬性的時候革娄,一定要小心不要隨便修改父函數(shù)內(nèi)部變量的值倾贰。
最后放兩個思考題:
思考題一:
var name =?"The Window";
var object =?{
name :?"My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());????//?打印出什么冕碟?
思考題二:
var name =?"The Window";
var object =?{
name :?"My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());????//?打印出什么?
參考文獻及資料:
《JavaScript權(quán)威指南》
阮一峰博客《學(xué)習(xí)JavaScript閉包(closure)》
如果你在本文中發(fā)現(xiàn)錯誤或者有異議的地方匆浙,可以在評論留言安寺,謝謝!