一师倔、介紹
作用域鏈就是根據(jù)在內(nèi)部函數(shù)可以訪問外部函數(shù)變量的這種機(jī)制役首,用鏈?zhǔn)讲檎覜Q定哪些數(shù)據(jù)能被內(nèi)部函數(shù)訪問虫啥。
想要知道js怎么鏈?zhǔn)讲檎艺鼻欤偷孟攘私鈐s的執(zhí)行環(huán)境
執(zhí)行環(huán)境(execution context)
每個(gè)函數(shù)運(yùn)行時(shí)都會產(chǎn)生一個(gè)執(zhí)行環(huán)境坑赡,而這個(gè)執(zhí)行環(huán)境怎么表示呢?js為每一個(gè)執(zhí)行環(huán)境關(guān)聯(lián)了一個(gè)變量對象么抗。環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對象中毅否。
全局執(zhí)行環(huán)境是最外圍的執(zhí)行環(huán)境,全局執(zhí)行環(huán)境被認(rèn)為是window對象蝇刀,因此所有的全局變量和函數(shù)都作為window對象的屬性和方法創(chuàng)建的螟加。
js的執(zhí)行順序是根據(jù)函數(shù)的調(diào)用來決定的,當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),該函數(shù)環(huán)境的變量對象就被壓入一個(gè)環(huán)境棧中捆探。而在函數(shù)執(zhí)行之后然爆,棧將該函數(shù)的變量對象彈出,把控制權(quán)交給之前的執(zhí)行環(huán)境變量對象黍图。這個(gè)就是剛才說的鏈?zhǔn)浇Y(jié)構(gòu)曾雕。
JavaScript 變量可以是局部變量或全局變量。
私有變量可以用到閉包助被。
閉包就是一個(gè)函數(shù)引用另一個(gè)函數(shù)的變量剖张,因?yàn)樽兞勘灰弥圆粫换厥眨虼丝梢杂脕矸庋b一個(gè)私有變量揩环。這是優(yōu)點(diǎn)也是缺點(diǎn)搔弄,不必要的閉包只會增加內(nèi)存消耗。
或者說閉包就是子函數(shù)可以使用父函數(shù)的局部變量丰滑,還有父函數(shù)的參數(shù)顾犹。
二、知識點(diǎn)介紹
1吨枉、作用域鏈
2蹦渣、閉包
3、閉包和作用域鏈
4貌亭、作用域鏈知識總結(jié)
5、this對象
三认臊、上課對應(yīng)視頻的說明文檔
1圃庭、作用域鏈
談起作用域鏈,我們就不得不從作用域開始談起失晴。因?yàn)樗^的作用域鏈就是由多個(gè)作用域組成的剧腻。那么, 什么是作用域呢涂屁?
1.1书在、什么是作用域
1.1.1作用域是一個(gè)函數(shù)在執(zhí)行時(shí)期的執(zhí)行環(huán)境。
執(zhí)行環(huán)境是JavaScript中的重要概念之一拆又。執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù)儒旬,決定了他們各自的行為。每個(gè)執(zhí)行環(huán)境都有一個(gè)與之關(guān)聯(lián)的變量對象帖族,環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對象中栈源。
全局執(zhí)行環(huán)境是最外圍的一個(gè)執(zhí)行環(huán)境。在Web瀏覽器中竖般,全局執(zhí)行環(huán)境被認(rèn)為是window對象甚垦,因此所有全局變量和函數(shù)都是作為window對象的屬性和方法創(chuàng)建的。某個(gè)執(zhí)行環(huán)境中的所有代碼執(zhí)行完畢后,該環(huán)境被銷毀艰亮,保存在其中的所有變量和函數(shù)定義也隨之銷毀(全局執(zhí)行環(huán)境知道應(yīng)用程序退出–例如關(guān)閉網(wǎng)頁或?yàn)g覽器—時(shí)才會被銷毀)
每個(gè)函數(shù)都有自己的執(zhí)行環(huán)境闭翩。當(dāng)執(zhí)行流進(jìn)入一個(gè)函數(shù)時(shí),函數(shù)的環(huán)境就會被推入一個(gè)環(huán)境棧中迄埃。而在函數(shù)執(zhí)行后疗韵,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境调俘。
執(zhí)行環(huán)境的建立分為兩個(gè)階段:進(jìn)入執(zhí)行上下文(創(chuàng)建階段)和執(zhí)行階段(激活/執(zhí)行階段)
1)進(jìn)入上下文階段:發(fā)生在函數(shù)調(diào)用時(shí)伶棒,但在執(zhí)行具體代碼之前。具體完成創(chuàng)建作用域鏈彩库;創(chuàng)建變量肤无、函數(shù)和參數(shù)以及求this的值
2)執(zhí)行代碼階段:主要完成變量賦值、函數(shù)引用和解釋/執(zhí)行其他代碼
總的來說可以將執(zhí)行上下文看作是一個(gè)對象
EC = {
VO:{/*函數(shù)中的arguments對象骇钦、參數(shù)宛渐、內(nèi)部變量以及函數(shù)聲明*/}
this:{},
Scope:{/*VO以及所有父執(zhí)行上下文中的VO*/}
}
每一個(gè)函數(shù)在執(zhí)行的時(shí)候都有著其特有的執(zhí)行環(huán)境,ECMAScript標(biāo)準(zhǔn)規(guī)定眯搭,在javascript中只有函數(shù)才擁有作用域窥翩。換句話,也就是說鳞仙,JS中不存在塊級作用域寇蚊。比如下面這樣:
function getA() {
if (false) {
var a = 1;
}
console.log(a);? //undefined
}
getA();function getB() {
console.log(b);
}
getB();? ? // ReferenceError: b is not defined
上面的兩段代碼,區(qū)別在于 :getA()函數(shù)中棍好,有變量a的聲明仗岸,而getB()函數(shù)中沒有變量b的聲明。
另外還有一點(diǎn)借笙,關(guān)于作用域中的聲明提前扒怖。
1.1.2.作用域中聲明提前
在上面的getA()函數(shù)中,或許你還存在著疑惑业稼,為什么a="undefined"呢盗痒,具體原因就是因?yàn)樽饔糜蛑械穆暶魈崆埃核詆etA()函數(shù)和下面的寫法是等價(jià)的:
function getA(){
var a;
if(false){
a=1
};
console.log(a);
}
既然提到變量的聲明提前,那么只需要搞清楚三個(gè)問題即可:
1)什么是變量
2)什么是變量聲明
3)聲明提前到什么時(shí)候低散。
1)什么是變量俯邓?
每一個(gè)執(zhí)行環(huán)境都對應(yīng)一個(gè)變量對象,在該執(zhí)行環(huán)境中定義的所有變量和函數(shù)都存放在其對應(yīng)的變量對象中谦纱。
1)進(jìn)入執(zhí)行上下文時(shí)看成,VO的初始化過程如下:
函數(shù)的形參:變量對象的一個(gè)屬性,其屬性名就是形參的名字跨嘉,其值就是實(shí)參的值川慌;對于沒有傳遞的參數(shù)吃嘿,其值為undefined;
函數(shù)聲明:變量對象的一個(gè)屬性梦重,其屬性名和屬性值都是函數(shù)對象創(chuàng)建出來的兑燥,如果變量對象已經(jīng)辦好了相同名字的屬性,則替換它的值
變量聲明:變量對象的一個(gè)屬性琴拧,其屬性名即為變量名降瞳,其值為undefined;如果變量名和已經(jīng)聲明的函數(shù)名或者函數(shù)的參數(shù)名蚓胸,則不會影響已經(jīng)存在的屬性
2)執(zhí)行代碼階段挣饥,變量對象中的一些屬性undefined值將會確定
這里需要說明一下:函數(shù)表達(dá)式不包含在變量對象之中
var foo = 10;?
function bar() {} // function declaration, FD?
(function baz() {}); // function expression, FE?
console.log(?
this.foo == foo, // true?
window.bar == bar // true?
);?
console.log(baz); // ReferenceError, "baz" is not defined?
之后,全局上下文的變量對象為
變量包括兩種沛膳,普通變量和函數(shù)變量扔枫。
普通變量:凡是用var標(biāo)識的都是普通變量。比如下面 :
var x=1;? ? ? ? ? ? ? var object={};var? getA=function(){};? //以上三種均是普通變量锹安,但是這三個(gè)等式都具有賦值操作短荐。所以,要分清楚聲明和賦值叹哭。聲明是指 var x; 賦值是指 x=1;
函數(shù)變量:函數(shù)變量特指的是下面的這種忍宋,fun就是一個(gè)函數(shù)變量。
function fun(){} ;// 這是指函數(shù)變量. 函數(shù)變量一般也說成函數(shù)聲明风罩。
類似下面這樣糠排,不是函數(shù)聲明,而是函數(shù)表達(dá)式
var getA=function(){}? ? ? //這是函數(shù)表達(dá)式var getA=function fun(){}; //這也是函數(shù)表達(dá)式超升,不存在函數(shù)聲明乳讥。關(guān)于函數(shù)聲明和函數(shù)表達(dá)式的區(qū)別,詳情見javascript系列---函數(shù)篇第二部分
2)什么是變量聲明廓俭?
變量有普通變量和函數(shù)變量,所以變量的聲明就有普通變量聲明和函數(shù)變量聲明唉工。
普通變量聲明
var x=1; //聲明+賦值var object={};? //聲明+賦值
上面的兩個(gè)變量執(zhí)行的時(shí)候總是這樣的
var x = undefined;? ? ? //聲明var object = undefined; //聲明
x = 1;? ? ? ? ? ? ? ? ? //賦值
object = {};? ? ? ? ? ? //賦值
關(guān)于聲明和賦值研乒,請注意,聲明是在函數(shù)第一行代碼執(zhí)行之前就已經(jīng)完成淋硝,而賦值是在函數(shù)執(zhí)行時(shí)期才開始賦值雹熬。所以,聲明總是存在于賦值之前谣膳。而且竿报,普通變量的聲明時(shí)期總是等于undefined.
函數(shù)變量聲明
函數(shù)變量聲明指的是下面這樣的:
function getA(){}; //函數(shù)聲明
3)聲明提前到什么時(shí)候?
所有變量的聲明继谚,在函數(shù)內(nèi)部第一行代碼開始執(zhí)行的時(shí)候就已經(jīng)完成烈菌。-----聲明的順序見
1.2、活動(dòng)對象
當(dāng)函數(shù)被調(diào)用的時(shí)候,一個(gè)特殊的對象–活動(dòng)對象將會被創(chuàng)建芽世。這個(gè)對象中包含形參和arguments對象挚赊。活動(dòng)對象之后會作為函數(shù)上下文的變量對象來使用济瓢。換句話說荠割,活動(dòng)對象除了變量和函數(shù)聲明之外,它還存儲了形參和arguments對象旺矾。
1.3蔑鹦、作用域詳解
由以上介紹可知,當(dāng)某個(gè)函數(shù)被調(diào)用時(shí)箕宙,會創(chuàng)建一個(gè)執(zhí)行環(huán)境及相應(yīng)的作用域鏈嚎朽。然后,使用arguments和其他命名參數(shù)的值來初始化函數(shù)的活動(dòng)對象扒吁。但在作用域鏈中火鼻,外部函數(shù)的活動(dòng)對象始終處于第二位,外部函數(shù)的外部函數(shù)對象處于第三位……直至作為作用域終點(diǎn)的全局執(zhí)行環(huán)境
函數(shù)的作用域雕崩,也就是函數(shù)的執(zhí)行環(huán)境魁索,所以函數(shù)作用域內(nèi)肯定保存著函數(shù)內(nèi)部聲明的所有的變量。
一個(gè)函數(shù)在執(zhí)行時(shí)所用到的變量無外乎來源于下面三種:
1)函數(shù)的參數(shù)----來源于函數(shù)內(nèi)部的作用域
2)在函數(shù)內(nèi)部聲明的變量(普通變量和函數(shù)變量)----也來源于函數(shù)內(nèi)部作用域
3)來源于函數(shù)的外部作用域的變量盼铁,放在1.3中講粗蔚。
比如下面這樣:
var x = 1;function add(num) () {
var y = 1;
return x + num + y;? //x來源于外部作用域,num來源于參數(shù)(參數(shù)也屬于內(nèi)部作用域)饶火,y來源于內(nèi)部作用域鹏控。
}
那么一個(gè)函數(shù)的作用域到底是什么呢?
在一個(gè)函數(shù)被調(diào)用的時(shí)候肤寝,函數(shù)的作用域才會存在当辐。此時(shí),在函數(shù)還沒有開始執(zhí)行的時(shí)候鲤看,開始創(chuàng)建函數(shù)的作用域:
函數(shù)作用域的創(chuàng)建步驟:
1)函數(shù)形參的聲明缘揪。
2)函數(shù)變量的聲明
3)普通變量的聲明。?
4)函數(shù)內(nèi)部的this指針賦值
5)函數(shù)內(nèi)部代碼開始執(zhí)行义桂!?
所以找筝,在這里也解釋了,為什么說函數(shù)被調(diào)用時(shí)慷吊,聲明提前袖裕,在創(chuàng)建函數(shù)作用域的時(shí)候就會先聲明各種變量。
關(guān)于變量的聲明溉瓶,這里有幾點(diǎn)需要強(qiáng)調(diào)
1)函數(shù)形參在聲明的時(shí)候已經(jīng)指定其形參的值急鳄。?
function add(num) {
var num;
console.log(num);? //1
}
add(1);
2)在第二步函數(shù)變量的生命中谤民,函數(shù)變量會覆蓋以前聲明過的同名聲明。
function add(num1, fun2) {
function fun2() {
var x = 2;
}
console.log(typeof num1); //function?
console.log(fun2.toString()) //functon fun2(){ var x=2;}
}
add(function () {
}, function () {
var x = 1
});
3)在第三步中攒岛,普通變量的聲明赖临,不會覆蓋以前的同名參數(shù)
function add(fun,num) {
var fun,num;
console.log(typeof fun) //function
console.log(num);? ? ? //1
}
add(function(){},1);
在所有的聲明結(jié)束后,函數(shù)才開始執(zhí)行代碼T志狻>ふァ!
function compare(value1,value2){
if(value1 < value2){
return -1;
} else if( value1 > value2 ) {
return 1;
} else {
return 0;
}
}
以上代碼定義了compare()函數(shù)顺饮,然后又在全局作用域中調(diào)用了它吵聪。當(dāng)調(diào)用compare()時(shí),會創(chuàng)建一個(gè)包含arguments兼雄、value1吟逝、value2的活動(dòng)對象。全局執(zhí)行環(huán)境的變量對象(包含result和compare)在compare()執(zhí)行環(huán)境的作用域鏈中則處于第二位赦肋。下圖包含了上述關(guān)系的compare()函數(shù)執(zhí)行時(shí)的作用域鏈块攒。
后臺的每個(gè)執(zhí)行環(huán)境都有一個(gè)表示變量的對象——變量對象。全局環(huán)境的變量對象始終存在佃乘,而像compare()函數(shù)這樣的局部環(huán)境的變量對象囱井,則只在函數(shù)執(zhí)行的過程中存在。在創(chuàng)建compare()函數(shù)時(shí)趣避,會創(chuàng)建一個(gè)預(yù)先包含全局變量對象的作用域鏈庞呕,這個(gè)作用域鏈會被保存在內(nèi)部的[[Scope]]屬性中。當(dāng)調(diào)用compare()函數(shù)時(shí)程帕,會為函數(shù)創(chuàng)建一個(gè)執(zhí)行環(huán)境住练,然后通過賦值函數(shù)的[[Scope]]屬性中的對象構(gòu)建起執(zhí)行環(huán)境的作用域鏈。此后愁拭,又有一個(gè)活動(dòng)對象別創(chuàng)建并被推入執(zhí)行環(huán)境作用域鏈的前端讲逛。對于這個(gè)例子中,compare()函數(shù)的執(zhí)行函數(shù)而言岭埠,其作用域鏈中包含兩個(gè)變量對象:本地活動(dòng)對象和全局便朗對象妆绞。作用域鏈本質(zhì)上是一個(gè)指向變量對象的指針列表,它只引用但不實(shí)際包含變量對象枫攀。
1.4.作用域鏈的組成
在JS中,函數(shù)的可以允許嵌套的株茶。即来涨,在一個(gè)函數(shù)的內(nèi)部聲明另一個(gè)函數(shù)
類似這樣:
function A(){
var? a=1;
function B(){? //在A函數(shù)內(nèi)部,聲明了函數(shù)B启盛,這就是所謂的函數(shù)嵌套蹦掐。
var b=2;?
}
}
對于A來說技羔,A函數(shù)在執(zhí)行的時(shí)候,會創(chuàng)建其A函數(shù)的作用域卧抗, 那么函數(shù)B在創(chuàng)建的時(shí)候尽纽,會引用A的作用域念逞,類似下面這樣
函數(shù)B在執(zhí)行的時(shí)候,其作用域類似于下面這樣:
從上面的兩幅圖中可以看出,函數(shù)B在執(zhí)行的時(shí)候呐萌,是會引用函數(shù)A的作用域的。所以丐枉,像這種函數(shù)作用域的嵌套就組成了所謂的函數(shù)作用域鏈朱沃。當(dāng)在自身作用域內(nèi)找不到該變量的時(shí)候,會沿著作用域鏈逐步向上查找嗜傅,若在全局作用域內(nèi)部仍找不到該變量金句,則會拋出異常。
2吕嘀、閉包
2.1违寞、什么是閉包
一般來說,當(dāng)某個(gè)環(huán)境中的所有代碼執(zhí)行完畢后偶房,該環(huán)境被銷毀(彈出環(huán)境棧)趁曼,保存在其中的所有變量和函數(shù)也隨之銷毀(全局執(zhí)行環(huán)境變量直到應(yīng)用程序退出,如網(wǎng)頁關(guān)閉才會被銷毀)
但是像上面那種有內(nèi)部函數(shù)的又有所不同蝴悉,當(dāng)outer()函數(shù)執(zhí)行結(jié)束彰阴,執(zhí)行環(huán)境被銷毀,但是其關(guān)聯(lián)的活動(dòng)對象并沒有隨之銷毀拍冠,而是一直存在于內(nèi)存中尿这,因?yàn)樵摶顒?dòng)對象被其內(nèi)部函數(shù)的作用域鏈所引用。
具體如下圖:
outer執(zhí)行結(jié)束庆杜,內(nèi)部函數(shù)開始被調(diào)用
outer執(zhí)行環(huán)境等待被回收射众,outer的作用域鏈對全局變量對象和outer的活動(dòng)對象引用都斷了
像上面這種內(nèi)部函數(shù)的作用域鏈仍然保持著對父函數(shù)活動(dòng)對象的引用,就是閉包(closure)
2.2晃财、閉包的作用
閉包有兩個(gè)作用:
第一個(gè)就是可以讀取自身函數(shù)外部的變量(沿著作用域鏈尋找)
第二個(gè)就是讓這些外部變量始終保存在內(nèi)存中
關(guān)于第二點(diǎn)叨橱,來看一下以下的代碼:
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){//注:i是outer()的局部變量
result[i] = function(){
return i;
}
}
return result;//返回一個(gè)函數(shù)對象數(shù)組
//這個(gè)時(shí)候會初始化result.length個(gè)關(guān)于內(nèi)部函數(shù)的作用域鏈
}
var fn = outer();
console.log(fn[0]());//result:2
console.log(fn[1]());//result:2
</script>
返回結(jié)果很出乎意料吧,你肯定以為依次返回0断盛,1罗洗,但事實(shí)并非如此
來看一下調(diào)用fn[0]()的作用域鏈圖:
可以看到result[0]函數(shù)的活動(dòng)對象里并沒有定義i這個(gè)變量,于是沿著作用域鏈去找i變量钢猛,結(jié)果在父函數(shù)outer的活動(dòng)對象里找到變量i(值為2)伙菜,而這個(gè)變量i是父函數(shù)執(zhí)行結(jié)束后將最終值保存在內(nèi)存里的結(jié)果。
由此也可以得出命迈,js函數(shù)內(nèi)的變量值不是在編譯的時(shí)候就確定的贩绕,而是等在運(yùn)行時(shí)期再去尋找的火的。
那怎么才能讓result數(shù)組函數(shù)返回我們所期望的值呢?
看一下result的活動(dòng)對象里有一個(gè)arguments淑倾,arguments對象是一個(gè)參數(shù)的集合馏鹤,是用來保存對象的。
那么我們就可以把i當(dāng)成參數(shù)傳進(jìn)去娇哆,這樣一調(diào)用函數(shù)生成的活動(dòng)對象內(nèi)的arguments就有當(dāng)前i的副本湃累。
改進(jìn)之后:
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個(gè)帶參函數(shù)
function arg(num){
return num;
}
//把i當(dāng)成參數(shù)傳進(jìn)去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]);//result:0
console.log(fn[1]);//result:1
</script>
雖然得到了期望的結(jié)果,但是又有人問這算閉包嗎迂尝?調(diào)用內(nèi)部函數(shù)的時(shí)候脱茉,父函數(shù)的環(huán)境變量還沒被銷毀呢,而且result返回的是一個(gè)整型數(shù)組垄开,而不是一個(gè)函數(shù)數(shù)組琴许!
確實(shí)如此,那就讓arg(num)函數(shù)內(nèi)部再定義一個(gè)內(nèi)部函數(shù)就好了:
這樣result返回的其實(shí)是innerarg()函數(shù)
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個(gè)帶參函數(shù)
function arg(num){
function innerarg(){
return num;
}
return innerarg;
}
//把i當(dāng)成參數(shù)傳進(jìn)去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]());
console.log(fn[1]());
</script>
當(dāng)調(diào)用outer溉躲,for循環(huán)內(nèi)i=0時(shí)的作用域鏈圖如下:
由上圖可知榜田,當(dāng)調(diào)用innerarg()時(shí),它會沿作用域鏈找到父函數(shù)arg()活動(dòng)對象里的arguments參數(shù)num=0.
上面代碼中锻梳,函數(shù)arg在outer函數(shù)內(nèi)預(yù)先被調(diào)用執(zhí)行了箭券,對于這種方法,js有一種簡潔的寫法
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個(gè)帶參函數(shù)
result[i] = function(num){
function innerarg(){
return num;
}
return innerarg;
}(i);//預(yù)先執(zhí)行函數(shù)寫法
//把i當(dāng)成參數(shù)傳進(jìn)去
}
return result;
}
閉包的概念:有權(quán)訪問另一個(gè)作用域的函數(shù)疑枯。
這句話就告訴我們辩块,第一,閉包是一個(gè)函數(shù)荆永。第二废亭,閉包是一個(gè)能夠訪問另一個(gè)函數(shù)作用域。
那么具钥,類似下面這樣豆村,
function A(){
var a=1;
function B(){? //閉包函數(shù),函數(shù)b能夠訪問函數(shù)a的作用域骂删。所以掌动,像類似這么樣的函數(shù),我們就稱為閉包
}
}
所以宁玫,創(chuàng)建閉包的方式就是在一個(gè)函數(shù)的內(nèi)部粗恢,創(chuàng)建另外一個(gè)函數(shù)。那么欧瘪,當(dāng)外部函數(shù)被調(diào)用的時(shí)候适滓,內(nèi)部函數(shù)也就隨著創(chuàng)建,這樣就形成了閉包。比如下面凭迹。
var fun = undefined;function a() {
var a = 1;
fun = function () {
}
}
2.3、閉包所引起的問題
其實(shí)苦囱,理解什么是閉包并不難嗅绸,難的是閉包很容易引起各種各樣的問題。
2.3.1撕彤、變量污染
看下面的這道例題:
var funB,
funC;
(function() {
var a = 1;
funB = function () {
a = a + 1;
console.log(a);
}
funC = function () {
a = a + 1;
console.log(a);
}
}());
funB();? //2
funC();? //3.
對于 funB和funC兩個(gè)閉包函數(shù)鱼鸠,無論是哪個(gè)函數(shù)在運(yùn)行的時(shí)候,都會改變匿名函數(shù)中變量a的值羹铅,這種情況就會污染了a變量蚀狰。
兩個(gè)函數(shù)的在運(yùn)行的時(shí)候作用域如下圖:
在這幅圖中,變量a可以被函數(shù)funB和funC改變职员,就相當(dāng)于外部作用域鏈上的變量對內(nèi)部作用域來說都是靜態(tài)的變量麻蹋,這樣,就很容易造成變量的污染焊切。還有一道最經(jīng)典的關(guān)于閉包的例題:
var array = [
];for (var i = 0; i < 10; i++) {
var fun = function () {
console.log(i);
}
array.push(fun);
}var index = array.length;while (index > 0) {
array[--index]();
} //輸出結(jié)果 全是10扮授;
想這種類似問題產(chǎn)生的根源就在于,沒有注意到外部作用域鏈上的所有變量均是靜態(tài)的专肪。
所以刹勃,為了解決這種變量的污染問題---而引入的閉包的另外一種使用方式。
那么它是如何解決這種變量污染的呢嚎尤?? 思想就是: 既然外部作用域鏈上的變量時(shí)靜態(tài)的荔仁,那么將外部作用域鏈上的變量拷貝到內(nèi)部作用域不就可以啦!芽死! 具體怎么拷貝乏梁,當(dāng)然是通過函數(shù)傳參的形式啊。
以第一道例題為例:
var funB,funC;
(function () {
var a = 1;
(function () {
funB = function () {
a = a + 1;
console.log(a);
}
}(a));
(function (a) {
funC = function () {
a = a + 1;
console.log(a);
}
}(a));
}());
funB()||funC();? //輸出結(jié)果全是2 另外也沒有改變作用域鏈上a的值收奔。
在函數(shù)執(zhí)行時(shí)掌呜,內(nèi)存的結(jié)構(gòu)如圖所示:
由圖中內(nèi)存結(jié)構(gòu)示意圖可見,為了解決閉包的這種變量污染的問題坪哄,而加了一層函數(shù)嵌套(通過匿名函數(shù)自執(zhí)行)质蕉,這種方式延長了閉包函數(shù)的作用域鏈。
2.3.2翩肌、內(nèi)存泄露
內(nèi)存泄露其實(shí)嚴(yán)格來說模暗,就是內(nèi)存溢出了,所謂的內(nèi)存溢出念祭,當(dāng)時(shí)就是內(nèi)存空間不夠用了啊兑宇。
那么,閉包為什么會引起內(nèi)存泄露呢粱坤?
var fun = undefined;function A() {
var a = 1;
fun = function () {
}
}
看上面的例題隶糕,只要函數(shù)fun存在瓷产,那么函數(shù)A中的變量a就會一直存在。也就是說枚驻,函數(shù)A的作用域一直得不到釋放濒旦,函數(shù)A的作用域鏈也不能得到釋放。如果再登,作用域鏈上沒有很多的變量尔邓,這種犧牲還可有可無,但是如果牽扯到DOM操作呢锉矢?
var element = document.getElementById('myButton');
(function () {
var myDiv = document.getElementById('myDiv')
element.onclick = function () {
//處理程序? }
}())
像這樣梯嗽,變量myDiv如果是一個(gè)占用內(nèi)存很大的DOM....如果持續(xù)這么下去,內(nèi)存空間豈不是一直得不到釋放沽损。久而久之灯节,變引起了內(nèi)存泄露(也是就內(nèi)存空間不足)。
3缠俺、閉包與作用域鏈
無論什么時(shí)候在函數(shù)中訪問一個(gè)變量時(shí)显晶,就會從作用域鏈中搜索具有相應(yīng)名字的變量。一般來講壹士,當(dāng)函數(shù)執(zhí)行完畢后磷雇,局部活動(dòng)對象就會被銷毀,內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對象)躏救。但是閉包的情況又有所不同唯笙。
function createComparisionFunction(propertyName) {
return function(object1,object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1 < value2){
return -1;
} else if( value1 > value2 ) {
return 1;
} else {
return 0;
}
}
}
在另一個(gè)函數(shù)內(nèi)部定義的函數(shù)會將包含函數(shù)(即外部函數(shù))的活動(dòng)對象添加到它的作用域鏈中。因此盒使,在createComparisonFunction()函數(shù)內(nèi)部定義的匿名函數(shù)作用域鏈中崩掘,實(shí)際上將會包含外部函數(shù)createComparisonFunction()的活動(dòng)對象。
var compare = createComparisonFunction('name');var result = compare({name:'Nicolas'},{name:'Greg'});
//解除對匿名函數(shù)的引用少办,以便釋放內(nèi)存
compareName = null;
當(dāng)上述代碼執(zhí)行時(shí)苞慢,下圖展示了包含函數(shù)與內(nèi)部匿名函數(shù)的作用域鏈
在匿名函數(shù)從createComparisonFunction()中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()函數(shù)的活動(dòng)對象和全局變量對象英妓。這樣挽放,匿名函數(shù)就可以訪問在createComparisonFunction()中定義的所有變量。更為重要的是蔓纠, createComparisonFunction()函數(shù)在執(zhí)行完畢后辑畦,其活動(dòng)對象也不會被銷毀,因?yàn)槟涿瘮?shù)的作用域鏈仍然在引用這個(gè)活動(dòng)對象腿倚。即當(dāng)createComparisonFunction()函數(shù)返回后纯出,其執(zhí)行環(huán)境的作用域鏈會被銷毀,但它的活動(dòng)對象任然會留在內(nèi)存中;直到匿名函數(shù)被銷毀后暂筝,createComparisonFunction()的活動(dòng)對象才會被銷毀箩言。
4、作用域鏈知識總結(jié)
當(dāng)代碼在一個(gè)環(huán)境中執(zhí)行時(shí)焕襟,都會創(chuàng)建一個(gè)作用域鏈分扎。 作用域鏈的用途是保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問。整個(gè)作用域鏈的本質(zhì)是一個(gè)指向變量對象的指針列表胧洒。作用域鏈的最前端,始終是當(dāng)前正在執(zhí)行的代碼所在環(huán)境的變量對象墨状。
如果這個(gè)環(huán)境是函數(shù)卫漫,則將其活動(dòng)對象(activation object)作為變量對象∩錾埃活動(dòng)對象在最開始時(shí)只包含一個(gè)變量列赎,就是函數(shù)內(nèi)部的arguments對象。作用域鏈中的下一個(gè)變量對象來自該函數(shù)的包含環(huán)境镐确,而再下一個(gè)變量對象來自再下一個(gè)包含環(huán)境包吝。這樣,一直延續(xù)到全局執(zhí)行環(huán)境源葫,全局執(zhí)行環(huán)境的變量對象始終是作用域鏈中的最后一個(gè)對象诗越。
作用域
先來談?wù)勛兞康淖饔糜?/p>
變量的作用域無非就是兩種:全局變量和局部變量。
4.1.1息堂、全局作用域:
最外層函數(shù)定義的變量擁有全局作用域嚷狞,即對任何內(nèi)部函數(shù)來說,都是可以訪問的:
<script>
var outerVar = "outer";
function fn(){
console.log(outerVar);
}
fn();//result:outer
</script>
4.1.2荣堰、局部作用域:
和全局作用域相反床未,局部作用域一般只在固定的代碼片段內(nèi)可訪問到,而對于函數(shù)外部是無法訪問的振坚,最常見的例如函數(shù)內(nèi)部
<script>
function fn(){
var innerVar = "inner";
}
fn();
console.log(innerVar);// ReferenceError: innerVar is not defined
</script>
需要注意的是薇搁,函數(shù)內(nèi)部聲明變量的時(shí)候,一定要使用var命令渡八。如果不用的話啃洋,你實(shí)際上聲明了一個(gè)全局變量!
<script>
function fn(){
innerVar = "inner";
}
fn();
console.log(innerVar);// result:inner
</script>
再來看一個(gè)代碼:
<script>
var scope = "global";
function fn(){
console.log(scope);//result:undefined
var scope = "local";
console.log(scope);//result:local;
}
fn();
</script>
很有趣吧呀狼,第一個(gè)輸出居然是undefined裂允,原本以為它會訪問外部的全局變量(scope=”global”),但是并沒有哥艇。這可以算是javascript的一個(gè)特點(diǎn)绝编,只要函數(shù)內(nèi)定義了一個(gè)局部變量,函數(shù)在解析的時(shí)候都會將這個(gè)變量“提前聲明”:
<script>
var scope = "global";
function fn(){
var scope;//提前聲明了局部變量
console.log(scope);//result:undefined
scope = "local";
console.log(scope);//result:local;
}
fn();
</script>
然而,也不能因此草率地將局部作用域定義為:用var聲明的變量作用范圍起止于花括號之間十饥。
javascript并沒有塊級作用域
4.1.3窟勃、塊級作用域
像在C/C++中,花括號內(nèi)中的每一段代碼都具有各自的作用域逗堵,而且變量在聲明它們的代碼段之外是不可見的秉氧,比如下面的c語言代碼:
for(int i = 0; i < 10; i++){
//i的作用范圍只在這個(gè)for循環(huán)
}
printf("%d",&i);//error
但是javascript不同,并沒有所謂的塊級作用域蜒秤,javascript的作用域是相對函數(shù)而言的汁咏,可以稱為函數(shù)作用域:
<script>
for(var i = 1; i < 10; i++){
//coding
}
console.log(i); //10?
</script>
4.2、作用域鏈(Scope Chain)
那什么是作用域鏈作媚?
我的理解就是攘滩,根據(jù)在內(nèi)部函數(shù)可以訪問外部函數(shù)變量的這種機(jī)制,用鏈?zhǔn)讲檎覜Q定哪些數(shù)據(jù)能被內(nèi)部函數(shù)訪問纸泡。
想要知道js怎么鏈?zhǔn)讲檎移剩偷孟攘私鈐s的執(zhí)行環(huán)境
4.2.1、執(zhí)行環(huán)境(execution context)
每個(gè)函數(shù)運(yùn)行時(shí)都會產(chǎn)生一個(gè)執(zhí)行環(huán)境女揭,而這個(gè)執(zhí)行環(huán)境怎么表示呢蚤假?js為每一個(gè)執(zhí)行環(huán)境關(guān)聯(lián)了一個(gè)變量對象。環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對象中吧兔。
全局執(zhí)行環(huán)境是最外圍的執(zhí)行環(huán)境磷仰,全局執(zhí)行環(huán)境被認(rèn)為是window對象,因此所有的全局變量和函數(shù)都作為window對象的屬性和方法創(chuàng)建的掩驱。
js的執(zhí)行順序是根據(jù)函數(shù)的調(diào)用來決定的芒划,當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),該函數(shù)環(huán)境的變量對象就被壓入一個(gè)環(huán)境棧中欧穴。而在函數(shù)執(zhí)行之后民逼,棧將該函數(shù)的變量對象彈出,把控制權(quán)交給之前的執(zhí)行環(huán)境變量對象涮帘。
舉個(gè)例子:
<script>
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
</script>
上面代碼執(zhí)行情況演示:
了解了環(huán)境變量拼苍,再詳細(xì)講講作用域鏈。
當(dāng)某個(gè)函數(shù)第一次被調(diào)用時(shí)调缨,就會創(chuàng)建一個(gè)執(zhí)行環(huán)境(execution context)以及相應(yīng)的作用域鏈疮鲫,并把作用域鏈賦值給一個(gè)特殊的內(nèi)部屬性([scope])。然后使用this弦叶,arguments(arguments在全局環(huán)境中不存在)和其他命名參數(shù)的值來初始化函數(shù)的活動(dòng)對象(activation object)俊犯。當(dāng)前執(zhí)行環(huán)境的變量對象始終在作用域鏈的第0位。
以上面的代碼為例伤哺,當(dāng)?shù)谝淮握{(diào)用fn1()時(shí)的作用域鏈如下圖所示:
(因?yàn)閒n2()還沒有被調(diào)用燕侠,所以沒有fn2的執(zhí)行環(huán)境)
可以看到fn1活動(dòng)對象里并沒有scope變量者祖,于是沿著作用域鏈(scope chain)向后尋找,結(jié)果在全局變量對象里找到了scope绢彤,所以就返回全局變量對象里的scope值七问。
標(biāo)識符解析是沿著作用域鏈一級一級地搜索標(biāo)識符地過程。搜索過程始終從作用域鏈地前端開始茫舶,然后逐級向后回溯械巡,直到找到標(biāo)識符為止(如果找不到標(biāo)識符,通常會導(dǎo)致錯(cuò)誤發(fā)生
那作用域鏈地作用僅僅只是為了搜索標(biāo)識符嗎饶氏?
再來看一段代碼:
<script>
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn();
</script>
outer()內(nèi)部返回了一個(gè)inner函數(shù)讥耗,當(dāng)調(diào)用outer時(shí),inner函數(shù)的作用域鏈就已經(jīng)被初始化了(復(fù)制父函數(shù)的作用域鏈疹启,再在前端插入自己的活動(dòng)對象)葛账,具體如下圖:
5、this對象
關(guān)于閉包經(jīng)常會看到這么一道題:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());//result:The Window
《javascript高級程序設(shè)計(jì)》一書給出的解釋是:
this對象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中皮仁,this等于window,而當(dāng)函數(shù)被作為某個(gè)對象調(diào)用時(shí)菲宴,this等于那個(gè)對象贷祈。不過,匿名函數(shù)具有全局性喝峦,因此this對象同常指向window.