函數(shù)作用域
要理解閉包,必須從理解函數(shù)被調(diào)用時(shí)都會(huì)發(fā)生什么入手。
我們知道,每個(gè)javascript函數(shù)都是一個(gè)對(duì)象,其中有一些屬性我們可以訪問(wèn)到,有一些不可以訪問(wèn),這些屬性僅供JavaScript引擎存取,是隱式屬性。[[scope]]就是其中一個(gè)。
[[scope]]就是我們所說(shuō)的作用域,其中存儲(chǔ)了執(zhí)行期上下文的集合。由于這個(gè)集合呈鏈?zhǔn)芥溄?,我們把這種鏈?zhǔn)芥溄咏凶鲎饔糜蜴湣?/p>
當(dāng)函數(shù)被定義(創(chuàng)建)時(shí)有一個(gè)自己所在環(huán)境的作用域(GO全局作用域 ,若是在函數(shù)內(nèi)部,就是引用別人的作用域),當(dāng)函數(shù)被執(zhí)行時(shí),會(huì)將自己的獨(dú)一無(wú)二的AO(活動(dòng)對(duì)象,是使用arguments和該函數(shù)內(nèi)部的變量值初始化的活動(dòng)對(duì)象)執(zhí)行上下文放在前端,形成一個(gè)作用域鏈;當(dāng)該函數(shù)執(zhí)行完,自己的AO會(huì)被干掉,回到被定義時(shí)的狀態(tài)。
另外,變量的查找,就是找所在函數(shù)的作用域,首先從作用域的頂端開(kāi)始查找,找不到的情況下,會(huì)查找外部函數(shù)的活動(dòng)對(duì)象,依次向下查找,直到到達(dá)作為作用域鏈終點(diǎn)的全局執(zhí)行環(huán)境。
下面看幾個(gè)查找變量例子,深入理解函數(shù)作用域及作用域鏈。
function a(){
function b(){
var b=2223;
}
var a=78;
}
a()
b()
console.log(b)
輸出結(jié)果: error: b is not defined
當(dāng)函數(shù)a執(zhí)行完畢后,該函數(shù)內(nèi)部的活動(dòng)對(duì)象AO就會(huì)被銷(xiāo)毀。所以函數(shù)外部是訪問(wèn)不到函數(shù)內(nèi)部的變量的。
function outer(){
function inner(){
var b=2223;
a=0
}
var a=78;
inner() //①
console.log(a)
console.log(b)
}
outer()
輸出結(jié)果: 0 , error: b is not defined
當(dāng)函數(shù)inner在被定義的階段,就會(huì)擁有(引用)函數(shù)outer的作用域(包括函數(shù)outer自己局部的活動(dòng)對(duì)象AO和全局作用域);當(dāng)函數(shù)inner()被執(zhí)行的時(shí)候,會(huì)再創(chuàng)建一個(gè)自己的活動(dòng)對(duì)象AO并被推入執(zhí)行環(huán)境作用域鏈的前端。
inner()函數(shù)在被執(zhí)行的時(shí)候,由于變量a在outer()函數(shù)中已經(jīng)存在并被inner()引用,所以inner()函數(shù)內(nèi)部的變量a會(huì)修改掉外部函數(shù)變量a的值,并且可以不用聲明。當(dāng)inner()函數(shù)執(zhí)行完畢后(執(zhí)行到①處),inner()函數(shù)局部的AO會(huì)被銷(xiāo)毀,下面就訪問(wèn)不到變量b了,而且這時(shí)候變量a的值將是被inner()函數(shù)修改過(guò)的值。
function a(){
function b(){
var b=2223;
a=0
}
var a=78;
b=1
b()
console.log(b)
}
a()
輸出結(jié)果: 1
var x=10;
function a(){
console.log(x);
}
function b(){
var x=20;
a();
}
a();//10
b();//還是10;
總之:函數(shù)在被定義階段,會(huì)引用著其所在環(huán)境的作用域;執(zhí)行階段,會(huì)創(chuàng)建一個(gè)自己獨(dú)一無(wú)二的活動(dòng)對(duì)象AO,并推入執(zhí)行環(huán)境作用域鏈的前端;函數(shù)執(zhí)行完畢之后,自己的執(zhí)行上下文AO會(huì)被銷(xiāo)毀,所以,這時(shí)候訪問(wèn)其內(nèi)部的變量是訪問(wèn)不到的。但是,閉包的情況又有不同。
簡(jiǎn)述什么是閉包
閉包:有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)。
從理論的角度上,所有的JavaScript函數(shù)都是閉包,因?yàn)楹瘮?shù)在被定義階段就會(huì)存儲(chǔ)一個(gè)自己所在環(huán)境的作用域,可以訪問(wèn)這個(gè)作用域中的所有變量。可以說(shuō),閉包是 JS 函數(shù)作用域的副產(chǎn)品。理解js作用域,自然就明白了閉包,即使你不知道那是閉包。
從技術(shù)實(shí)踐的角度,以下函數(shù)才算閉包:
- 定義該函數(shù)的執(zhí)行環(huán)境的作用域(執(zhí)行上下文)即使被銷(xiāo)毀 ,它的活動(dòng)對(duì)象(AO)仍然會(huì)留在內(nèi)存中,被該函數(shù)引用著。
- 引用了函數(shù)體外部的變量。
創(chuàng)建閉包常見(jiàn)方式,就是在一個(gè)函數(shù)A內(nèi)部創(chuàng)建另一個(gè)函數(shù)B,然后通過(guò)return這個(gè)函數(shù)B以便在外部使用,這個(gè)函數(shù)B就是一個(gè)閉包。
舉個(gè)例子:
//也算閉包
var a = 1;
function foo() {
console.log(a);
}
foo();
//函數(shù)內(nèi)部定義函數(shù)
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()() //這里相當(dāng)于:
//var foo = checkscope();
//foo();
輸出結(jié)果:local scope
f()函數(shù)在被定義階段就被保存到了外部,這個(gè)時(shí)候就相當(dāng)于外部的函數(shù)可以訪問(wèn)另一個(gè)函數(shù)內(nèi)部的變量,f()函數(shù)會(huì)形成一個(gè)閉包。
按照函數(shù)作用域的概念,當(dāng)checkscope()執(zhí)行完畢后,其局部的活動(dòng)對(duì)象AO會(huì)被銷(xiāo)毀;但是由于checkscope()函數(shù)執(zhí)行完畢后返回一個(gè)函數(shù),根據(jù)函數(shù)在被定義階段會(huì)引用該函數(shù)所在執(zhí)行環(huán)境的執(zhí)行上下文,被返回的函數(shù)f()即使被保存到了外部依然引用著checkscope()函數(shù)的執(zhí)行期上下文,直到函數(shù)f()執(zhí)行完畢,checkscope()函數(shù)的執(zhí)行上下文才會(huì)被銷(xiāo)毀。
也就是說(shuō)被嵌套的函數(shù)f()無(wú)論在什么地方執(zhí)行,都會(huì)包含著外部函數(shù)(定義該函數(shù))的活動(dòng)對(duì)象。所以,即使f()被保存到外部,也可以訪問(wèn)到另一個(gè)函數(shù)checkscope()中定義的變量。
無(wú)論通過(guò)何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外, 它都會(huì)持有對(duì)原始定義作用域的引用, 無(wú)論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。
由此看來(lái),閉包可能會(huì)導(dǎo)致一個(gè)問(wèn)題:導(dǎo)致原有作用域鏈不釋放,造成內(nèi)存泄漏(內(nèi)存空間越來(lái)越少)??梢酝ㄟ^(guò)手動(dòng)將被引用的函數(shù)設(shè)為null,來(lái)解除對(duì)該函數(shù)的引用,以便釋放內(nèi)存。
閉包的作用
- 實(shí)現(xiàn)公有變量。
function a(){
var num=100;
function b(){
num++;
console.log(num)
}
return b;
}
var demo=a();
demo();//101
demo();//102
function test(){
var num=100;
function a(){
num++;
}
function b(){
num--;
}
return [a,b]
}
var demo=test()
demo[0]();//101
demo[1]();//100
//函數(shù)a和函數(shù)b引用的是同一個(gè)作用域。
- 實(shí)現(xiàn)私有變量。
閉包通常用來(lái)創(chuàng)建內(nèi)部變量,使得這些變量不能被外部隨意修改,同時(shí)又可以通過(guò)指定的函數(shù)接口來(lái)操作。
通過(guò)在立即執(zhí)行函數(shù)中return 將方法保存到外部等待調(diào)用,內(nèi)部的變量由于是私有的,外部訪問(wèn)不到,可防止污染全局變量,利于模塊化開(kāi)發(fā)。
var foo = ( function() {
var secret = 'secret';
// “閉包”內(nèi)的函數(shù)可以訪問(wèn) secret 變量,而secret變量對(duì)于外部卻是隱藏的
return {
get_secret: function () {
// 通過(guò)定義的接口來(lái)訪問(wèn) secret
return secret;
},
new_secret: function ( new_secret ) {
// 通過(guò)定義的接口來(lái)修改 secret
secret = new_secret;
}
};
} () );
foo.get_secret (); // 得到 'secret'
foo.secret; // undefined,訪問(wèn)不能
foo.new_secret ('a new secret'); // 通過(guò)函數(shù)接口,我們?cè)L問(wèn)并修改了secret 變量
foo.get_secret (); // 得到 'a new secret'
var name='bcd';
var init=(function (){
var name='abc';
function callName(){
console.log(name);
}
//其他方法
return function () {
callName();
//其他方法
}
}())
init () //abc
閉包經(jīng)典題
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
console.log(i);
};
}
return result;
}
var fun = createFunctions();
for(var i=0;i<10;i++){
fun[i]();
}
輸出結(jié)果:打印十個(gè)10
數(shù)組每個(gè)值都是一個(gè)函數(shù),每個(gè)函數(shù)對(duì)createFunctions()形成一個(gè)閉包,此時(shí)i都是引用createFunctions()中同一個(gè)i變量。
function test(){
var arr=[];
for(var i=0;i<10;i++){
(function(j){
arr[j]=function(){
console.log(j);
}
}(i))
}
console.log(i)//10 ,i還是10
return arr
}
var myArr=test();
for(var i=0;i<10;i++){
myArr[i]()
}
輸出結(jié)果:從0到9
這次依然把數(shù)組每個(gè)值賦為函數(shù),不同的是循環(huán)十次立即執(zhí)行函數(shù),并將當(dāng)前循環(huán)的i作為參數(shù)傳進(jìn)立即執(zhí)行函數(shù),由于參數(shù)是按值傳遞的,這樣就把當(dāng)前循環(huán)的i保存下來(lái)了。
閉包中的this對(duì)象
在閉包中使用this對(duì)象可能會(huì)導(dǎo)致一些問(wèn)題,結(jié)果往往不是預(yù)想的輸出結(jié)果。
看個(gè)例子:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
輸出結(jié)果:The Window
this對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中,this等于window,而當(dāng)函數(shù)被作為某個(gè)對(duì)象的方法調(diào)用時(shí),this等于那個(gè)對(duì)象。匿名函數(shù)往往具有全局性,這里可以這樣理解,沒(méi)有任何對(duì)象調(diào)用這個(gè)匿名函數(shù),雖然這個(gè)匿名函數(shù)擁有getNameFunc()的執(zhí)行上下文。
因?yàn)檫@個(gè)匿名函數(shù)擁有getNameFunc()的執(zhí)行上下文,通過(guò)把外部函數(shù)getNameFunc()作用域中的this對(duì)象保存在一個(gè)閉包能夠訪問(wèn)到的變量里,就可以讓閉包訪問(wèn)到該對(duì)象了。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
輸出結(jié)果:My Object
理解到這里,基本上就搞定了閉包了。