前言
對于js中的閉包,無論是老司機還是小白,我想,見得不能再多了,然而有時三言兩語卻很難說得明白,反正在我初學(xué)時是這樣的,腦子里雖有概念,但是卻道不出個所以然來,在面試中經(jīng)常會被用來吊自己的胃口,考察基礎(chǔ),雖然網(wǎng)上自己也看過不少相關(guān)閉包的文章,帖子,但貌似這玩意,越看越復(fù)雜,滿滿逼格高,生涉難懂的專業(yè)詞匯常常把自己帶到溝里去了,越看越迷糊,其實終歸結(jié)底,用楊絳先生的一句話就是:“你的問題在于代碼寫得太少,書讀得不夠多",其實在我看來前者是主要的,是后者的檢驗, 自知目標(biāo)搬磚20年(還差19年..),其實,閉包的應(yīng)用,在我們不經(jīng)意間就使用了,是無處不在的,盡管知道閉包是個麻煩的玩意,自己也經(jīng)常吃回頭草看看這家伙的,所謂出去混,遲早是要還的,今天,我就對閉包的一點理解作一點點總結(jié),關(guān)于閉包,我也一直在學(xué)習(xí)當(dāng)中...話說多了,都是故事,直接擼起袖子,開始干吧
正文從這里開始~
理解上下文和作用域
其實上下文與作用域是兩個不同的概念,有時我自己也經(jīng)常混淆,把它們視為是同一個東西,我們知道函數(shù)的每次調(diào)用都會有與之緊密相連的作用域和上下文,從本質(zhì)上說,作用域其實是基于函數(shù)的
,而上下文
是基于對象的
,也就是說作用域是涉及到它所被調(diào)用函數(shù)中的變量訪問,而調(diào)用方法和訪問屬性又存在著不同的調(diào)用場景(4種調(diào)用場景,函數(shù)調(diào)用,方法調(diào)用,構(gòu)造器函數(shù)調(diào)用,call(),apply()間接調(diào)用),而上下文始終是this
所代表的值,它是擁有控制當(dāng)前執(zhí)行代碼的對象的引用
變量作用域
在javascript中,作用域是執(zhí)行代碼的上下文(方法調(diào)用中this所代表的值),作用域有三種類型:全局作用域(Global scope
),局部作用域(Local/Function scope
,函數(shù)作用域)和eval作用域,在函數(shù)內(nèi)部使用var
定義的代碼,其作用域都是局部的,且只對該函數(shù)的其他表達(dá)式是可見的
,包括嵌套子函數(shù)中的代碼,局部變量只能在它被調(diào)用的作用域范圍內(nèi)進行讀和寫的操作,在全局作用域內(nèi)定義的變量從任何地方都是可以訪問的,因為它是作用域鏈中的最高層中的最后一個,在整個范圍內(nèi)都是可見的,注意在Es6之前是沒有塊級作用域的,而Es6后是有的,也就是說Esif
,while
,switch
,for
語句是有了塊級作用域的,可以使用let
關(guān)鍵字聲明變量,修正了var
關(guān)鍵字的缺點,注意let使用規(guī)則
看如下代碼所示:
* 全局變量與局部變量
*
* @global variable {variable="itclab"}
* @function myFun
* @local variable {variable="itclanCode",variable=24}
* @function otherFun
* @eval作用域 evalfun
*/
var variable = "itclan"; //全局變量
console.log("全局variable","=",variable); // 全局variable = itclan
// 函數(shù)表達(dá)式
var myFun = function(){
var variable = "itclanCode"; //局部變量
console.log("局部variable","=",variable); // 局部variable = itclanCode
var otherFun = function(){
var variable = 24; //局部變量
console.log("局部variable","=",variable); // 局部variable = 24
}
otherFun();
}
myFun();
eval("var evalfun = 20;console.log('evalfun作用域','=',evalfun)");// evalfun作用域 = 20
注意
- 函數(shù)可以嵌套函數(shù),并可以無限的嵌套下去,也就是可以創(chuàng)建無數(shù)的函數(shù)作用域和eval作用域,而
javascript
壞境只是用一個全局作用域 - 全局作用域(
global scope
)是作用域鏈中的最后一層 - 包含函數(shù)的函數(shù),會創(chuàng)建堆棧執(zhí)行的作用域,這些鏈接在一起的棧通常被稱為作用域鏈(也就是后面會提到閉包產(chǎn)生的本質(zhì)原因)
什么是執(zhí)行壞境
所謂執(zhí)行壞境,它定義了變量或函數(shù)有訪問的其他數(shù)據(jù)的能力,它決定了各自的行為,它的側(cè)重點在于函數(shù)的作用域,而并不是所要糾結(jié)的上下文,一旦函數(shù)一聲明定義,就會自動的分配產(chǎn)生了作用域,有著自己的執(zhí)行壞境,執(zhí)行壞境可以分為創(chuàng)建與執(zhí)行兩個階段,在創(chuàng)建階段,js解析器首先會創(chuàng)建一個變量對象(活動對象),它由定義在執(zhí)行壞境中的變量,函數(shù)聲明和參數(shù)組成,在這個階段,系統(tǒng)會自動的產(chǎn)生一個this
對象,作用域鏈會被初始化,隨之,this
的值也會被確定,第二階段,也就是代碼執(zhí)行,代碼會被解釋執(zhí)行,你會發(fā)現(xiàn),每個執(zhí)行壞境都有一個與之關(guān)聯(lián)的變量對象,執(zhí)行壞境中所有定義的變量和函數(shù)都保存在這個對象中,注意,我們是無法手動的訪問這個對象的,只有js解析器才能夠訪問它,其實也就是this,盡管很抽象,但是理解它還是蠻重要的
作用域鏈(詞法作用域)
當(dāng)javascript
查找與變量相關(guān)聯(lián)的值時,會遵循一定的規(guī)則,也就是沿著作用域鏈從當(dāng)前函數(shù)作用域內(nèi)逐級的向上查找,直到頂層全局作用域結(jié)束,若找到則返回該值,若無則返回undefined,這個鏈條是基于作用域的層次結(jié)構(gòu)的,一旦當(dāng)代碼在壞境中執(zhí)行時,會自動的創(chuàng)建一個變量對象的作用域鏈,其作用域鏈的用途也就是保證對執(zhí)行壞境的全局變量和具有訪問權(quán)限函數(shù)內(nèi)的局部變量定制特殊的規(guī)則,由內(nèi)到外有序的對變量或者函數(shù)進行訪問,作用域鏈包含了在壞境棧中的每個執(zhí)行壞境對應(yīng)的變量對象,通過作用域鏈可以決定變量的訪問與標(biāo)識符的解析,如下代碼所示:
* 作用域鏈變量的訪問
*
* @global variable {name="隨筆川跡"}
* @function fun1,fun2
* @local variable {oTherName="哇嘎嘎",AliasName = "川川"}
* @return {fun2,name,oTherName,AliasName}
* @return fun2,fun1函數(shù)的返回結(jié)果值為fun2的值
*
*
*/
var name = "隨筆川跡"; // 全局變量
var fun1 = function(){
var oTherName = "哇嘎嘎"; // 局部變量
var fun2 = function(){
var AliasName = "川川"; // 局部變量
AliasName = oTherName;
oTherName = AliasName;
return {name,oTherName,AliasName};
}
console.log(fun2());
return fun2();
}
//console.log(fun2()); // 若在全局作用域調(diào)用訪問fun()會失敗,顯示fun2 is not defined
console.log(fun1(),"name is","=",name)
當(dāng)我們分析這段代碼時,首先全局范圍全局變量name
,函數(shù)fun1嵌套fun2函數(shù),fun1,fun2函數(shù)內(nèi)局部變量分別為:oTherName
,AliasName
當(dāng)在函數(shù)fun2內(nèi),并未聲明name變量
便在該函數(shù)fun2內(nèi)進行了訪問,這是如何找到的?javascript首先在當(dāng)前fun2函數(shù)作用域內(nèi)查找一個名為name的變量,但是在fun2并未找到,于是它會查找它的父函數(shù)fun1的作用域內(nèi)進行查找,但是發(fā)現(xiàn)仍然沒有找到,于是在往外進行查找,結(jié)果在全局作用域范圍內(nèi)查找了name的值
于是找到了便把該值進行返回,若是在全局作用域內(nèi)還未找到則會返回undefined,注意在函數(shù)fun2作用域內(nèi),name,oTherName,AliasName都是可以訪問的,而在函數(shù)fun1函數(shù)作用域內(nèi)是訪問不了oTherName
的,因為它脫離了fun1
的函數(shù)的作用域嘛,我們知道在函數(shù)外是無法訪問函數(shù)里面的的變量的,訪問變量由內(nèi)向外進行查找是可以的,但是反之則就不行,從上圖的箭頭分析圖可知,內(nèi)部壞境中,是可以通過作用域鏈訪問它所有的外部壞境
但是在外部壞境是無法訪問內(nèi)部壞境中的任何變量和函數(shù),這點很重要,我們在函數(shù)嵌套函數(shù),并且進行函數(shù)調(diào)用時,要格外注意,如果在編程當(dāng)中出現(xiàn)這種函數(shù)is not defined
那么就是牽扯到函數(shù)作用域的問題了,在函數(shù)外是無法訪問函數(shù)內(nèi)的變量或者函數(shù)的,當(dāng)然這種問題是可以解決的,也就是后面提到的閉包
其實上面我們的代碼中就已經(jīng)無形用了閉包,匿名函數(shù)fun1,fun2就是個閉包,嵌套函數(shù)與被嵌套壞境的連接是線性的,有次序的,對于標(biāo)識符(也就是變量或者函數(shù)名查找)是從當(dāng)前函數(shù)作用域開始,沿著作用域鏈逐級的向上查找,直到最頂端全局變量壞境,若找到該值則返回,若無則返回undefined
注意
:理解作用域以及作用域鏈對理解原型鏈?zhǔn)呛苡袔椭?其實他們區(qū)別并不是很大,兩者都是通過位置體系(上下嵌套關(guān)系)和分層體系來查找值的方法,進而可以對變量或者函數(shù)進行讀和寫的操作,如下代碼所示:
var x = 5;
var fun1 = function(){
var y = 10;
var fun2 = function(){
var z = 20;
return z+y+x;
}
fun2();
return fun2();
}
console.log("x+y+z的和=",fun1()); //x+y+z的和= 35
javascript沒有塊級作用域
在Es6之前,如if,for,while,switch邏輯語句是無法創(chuàng)建作用域,也就是它后面的雙大括號并沒有域的作用,這才得式變量可以相互覆蓋,解決辦法,你可以使用es6的let關(guān)鍵字聲明變量,注意let的使用,如下代碼所示
var str = "itclan"; // 全局變量
console.log(str); // itclan
if(true){ // if邏輯語句
str = "itclanCode";
console.log(str); // itclanCode
for(var i = 0;i<=2;i++){
str = i;
console.log(str); // 0,1,2
}
console.log(str); // 2
}
console.log(str); // 2
因此,代碼在執(zhí)行過程中,從上到下,str是變化的,因為在Es6之前,沒有塊級作用域,只有全局作用域,函數(shù)作用域,eval()作用域
注意
在函數(shù)中應(yīng)用var
聲明變量,避免作用域的陷阱
javascript會將缺少var
的變量聲明,即便在函數(shù)或者封裝在函數(shù)中,都會視為全局變量作用域,而非局部作用域,我們是不應(yīng)該出現(xiàn)這種不要var聲明的,這樣會造成全局變量的污染,易混淆,如下代碼所示
* 如果不使用var來聲明變量,那么,該變量實際上是在全局作用域中定義,而不是局部作用域中定義(它本是在局部作用域中定義)
*
* @descortion:這樣很容易產(chǎn)生誤解,應(yīng)當(dāng)杜絕這么干
* @在函數(shù)內(nèi)定義的變量應(yīng)用var,當(dāng)然要在函數(shù)內(nèi)部創(chuàng)建或更改全局作用域內(nèi)的屬性就另當(dāng)別論了的
*
*
*/
var fun1Exp = function(){
var fun2Exp = function(){
name = "污葵"; // 沒有使用var,它相相當(dāng)于window.name
}
fun2Exp();
}
fun1Exp();
console.log({name});
// 相反,使用var的情況
var fun3Exp = function(){
var fun4Exp = function(){
var age = 20; //使用var,局部變量
}
fun4Exp();
}
fun3Exp();
console.log(age); //Uncaught ReferenceError: age is not defined,報錯的原因,age在fun4Exp函數(shù)作用域中,在函數(shù)外是訪問不了函數(shù)內(nèi)部的變量的
作用域是在函數(shù)定義時就確定的,而非調(diào)用時確定
因為函數(shù)決定作用域,又因為函數(shù)也是對象,也是一種數(shù)據(jù)類型,一樣可以像基本數(shù)據(jù)類型值一樣被作為值來傳遞,作用域就是根據(jù)函數(shù)定義時的位置確定的,而與該函數(shù)在哪里被調(diào)用無關(guān),其實就是詞法作用域,作用域鏈?zhǔn)窃谡{(diào)用函數(shù)之前創(chuàng)建,也是這樣,我們就可以創(chuàng)建閉包,我們常常是這么做的,讓函數(shù)向全局作用域返回一個嵌套函數(shù),但該函數(shù)仍然能夠通過作用域訪問它父函數(shù)的作用域,作用域鏈?zhǔn)窃诙x時確定的,并在函數(shù)內(nèi)部傳遞代碼不會改變作用域
如下代碼所示:
* 作用域鏈?zhǔn)窃诤瘮?shù)定義時位置確定的,而非函數(shù)調(diào)用位置,在函數(shù)內(nèi)部傳遞代碼不會改變作用域鏈
*
* @funtion expression parentFun
* @local variable localVal
* @return parentFun的返回值為一個匿名函數(shù),訪問該匿名函數(shù)外的變量
*
*/
var parentFun = function(){
var localVar = "itclan是個有溫度的公眾號";
return function(){ // 返回一個匿名函數(shù)
console.log(localVar);
}
}
var nestedFun = parentFun();//nestedFun引用parentFun函數(shù),把函數(shù)parentFun函數(shù)的返回值賦值給變量nestedFun
nestedFun(); // 輸出itclan是個有溫度的公眾號,因為返回的函數(shù)可以通過作用域鏈訪問到localVar變量
產(chǎn)生閉包的根本原因是作用域鏈
在通過上面的了解變量的作用域和作用域鏈后,相信你理解閉包就不難了,如下代碼所示:
* 閉包是由作用域鏈引用的
*
* @function expression countNum 匿名函數(shù)
* @local variable count
* @return 匿名函數(shù)
*
*/
var countNum = function(){
var count = 0;
return function(){ //調(diào)用countNum的時候返回嵌套的子函數(shù)
return ++count;// count在作用域鏈內(nèi)定義,父函數(shù)里
};
}(); // 匿名函數(shù)的立即調(diào)用,返回嵌套函數(shù)
// countNum(),上面的匿名函數(shù)后若不加括號調(diào)用,則返回的結(jié)果將是return 后面的函數(shù)的整體代碼
console.log(countNum()); // 1
console.log(countNum()); // 2
console.log(countNum()); // 3
當(dāng)每次調(diào)用countNum函數(shù)時,嵌套在該函數(shù)內(nèi)的匿名函數(shù)是可以訪問父函數(shù)(這里指的是countNum的)作用域的,其實這就是所謂的閉包,作用鏈就是閉包的橋梁,用來連接內(nèi)部函數(shù)與外部函數(shù)的關(guān)系,從而達(dá)到外部函數(shù)訪問內(nèi)部函數(shù)局部變量或者函數(shù)的目的,其中被嵌套函數(shù)就可以稱為是一個閉包
小結(jié)
- 產(chǎn)生閉包的原因是由作用域鏈引起的
- 函數(shù)嵌套函數(shù),被嵌套的函數(shù)就可以稱為閉包
- 子函數(shù)可以使用父函數(shù)的變量(訪問其他函數(shù)內(nèi)部的局部變量)
- 讓變量始終保存在內(nèi)存中,避免自動垃圾回收(其實上面的例子中就已經(jīng)用到了的)
- 對外提供公有屬性和方法
總結(jié):
整篇文章從理解上文和作用域開始,以及什么是執(zhí)行壞境,其產(chǎn)生閉包的原因是作用域鏈,并知道在Es6之前是沒有塊級作用域的概念的,并且作用域是在函數(shù)定義時就確定的,而非函數(shù)調(diào)用確定,在我的理解中編程其實很大一部分就是對數(shù)據(jù)進行讀和寫的操作
其中讀可以理解對定義變量數(shù)據(jù)的訪問,而寫可以理解賦值,引用,變更,改寫操作,當(dāng)然js中不像其他后臺語言的存儲數(shù)據(jù)類型那般復(fù)雜,基本就是基本數(shù)據(jù)類型和對象了
理解作用域以及作用域鏈對理解閉包是相當(dāng)?shù)闹匾?對后續(xù)的原型鏈以及繼承都是相關(guān)聯(lián)的,其實也不必抓著什么執(zhí)行壞境和上下文這些相對抽象的概念不放,我們只有在平時的使用當(dāng)中,稍稍留意就行,在應(yīng)用中結(jié)合理論進行驗證,當(dāng)然閉包的內(nèi)容遠(yuǎn)不及此..
以下是本篇提點概要
- 理解上下文和作用域,作用域是基于函數(shù)的,而上下文是基于對象的,雖然說函數(shù)也是對象,但是這里更多的是指對象直接量的表示法,上下文始終圍繞著this所代表的值,它是擁有控制當(dāng)前執(zhí)行代碼對象的引用
- 變量的作用域,在Es6之前沒有塊級作用域,而Es6有了塊級作用域,也就是if,,while,switch,for,若使用let關(guān)鍵字,則具備塊級作用域,也就是說定義在雙大括號內(nèi)的變量,在雙大括號內(nèi)的才起作用,一旦離開該范圍,就不起作用了
- 什么是執(zhí)行壞境,定義了變量或函數(shù)有訪問的其他數(shù)據(jù)的能力,它決定了各自的行為,它的側(cè)重點在于函數(shù)的作用域,而并不是所要糾結(jié)的上下文,分為創(chuàng)建壞境和執(zhí)行壞境
- 作用域鏈(詞法作用域),當(dāng)查找與變量相關(guān)聯(lián)的值時,會遵循一定的規(guī)則,也就是沿著作用域鏈從當(dāng)前函數(shù)作用域內(nèi)逐級的向上查找,直到頂層全局作用域結(jié)束,若找到則返回該值,若無則返回undefined
- javascript沒有塊級作用域,往往很多時候使用匿名函數(shù)自執(zhí)行來模擬塊級作用域
- 作用域是在函數(shù)定義時就確定的,而非調(diào)用時確定,作用域就是根據(jù)函數(shù)定義時的位置確定的,而與該函數(shù)在哪里被調(diào)用無關(guān),其實就是詞法作用域
- 產(chǎn)生閉包的根本原因是作用域鏈,見上小結(jié)