從屌絲到架構(gòu)師的飛越(JavaScript篇)-作用域鏈及閉包

一师倔、介紹

作用域鏈就是根據(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.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末势誊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子谣蠢,更是在濱河造成了極大的恐慌粟耻,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眉踱,死亡現(xiàn)場離奇詭異挤忙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谈喳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門册烈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人婿禽,你說我怎么就攤上這事赏僧。” “怎么了扭倾?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵淀零,是天一觀的道長。 經(jīng)常有香客問我膛壹,道長驾中,這世上最難降的妖魔是什么唉堪? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮哀卫,結(jié)果婚禮上巨坊,老公的妹妹穿的比我還像新娘。我一直安慰自己此改,他們只是感情好趾撵,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著共啃,像睡著了一般占调。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上移剪,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天究珊,我揣著相機(jī)與錄音,去河邊找鬼纵苛。 笑死剿涮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的攻人。 我是一名探鬼主播取试,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼怀吻!你這毒婦竟也來了瞬浓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤蓬坡,失蹤者是張志新(化名)和其女友劉穎猿棉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屑咳,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萨赁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了兆龙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片位迂。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖详瑞,靈堂內(nèi)的尸體忽然破棺而出掂林,到底是詐尸還是另有隱情,我是刑警寧澤坝橡,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布泻帮,位于F島的核電站,受9級特大地震影響计寇,放射性物質(zhì)發(fā)生泄漏锣杂。R本人自食惡果不足惜脂倦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望元莫。 院中可真熱鬧赖阻,春花似錦、人聲如沸踱蠢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茎截。三九已至苇侵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間企锌,已是汗流浹背榆浓。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撕攒,地道東北人陡鹃。 一個(gè)月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像抖坪,于是被迫代替她去往敵國和親杉适。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容