JS基礎(chǔ)知識:變量對象畔柔、作用域鏈和閉包
前言:這段時間一直在消化作用域鏈和閉包的相關(guān)知識氯夷。之前看《JS高程》和一些技術(shù)博客,對于這些概念的論述多多少少不太清楚或者不太完整靶擦,包括一些大神的技術(shù)文章腮考。這也給我的學(xué)習(xí)上造成了一些困惑,這幾個概念的理解也是始終處于一個半懂不懂的狀態(tài)玄捕。后來在某公眾號看到了維客館的基礎(chǔ)文章踩蔚,這應(yīng)該是我所看到的最清楚,最全面枚粘,最好懂的文章了馅闽。所以我在學(xué)習(xí)之余決定寫一篇文章,總結(jié)學(xué)到的知識點(diǎn)赌结,用我的理解來闡述颈嚼,不足之處,見請諒解碳却。
執(zhí)行上下文(Execution Context)
也叫執(zhí)行環(huán)境泥兰,也可以簡稱“環(huán)境”。是JS在執(zhí)行過程中產(chǎn)生的量承,當(dāng)JS執(zhí)行一段可執(zhí)行的代碼時搬设,就會生成一個叫執(zhí)行環(huán)境的東西。JS中每個函數(shù)都會有自己的執(zhí)行環(huán)境撕捍,當(dāng)函數(shù)執(zhí)行時拿穴,就生成了它的執(zhí)行環(huán)境,執(zhí)行上下文會生成函數(shù)的作用域忧风。
除了函數(shù)有執(zhí)行環(huán)境默色,還有全局的環(huán)境。在JS中狮腿,往往不止一個執(zhí)行環(huán)境腿宰。
讓我們先來看一個栗子
var a=10;
function foo(){
? ? var b=5;
? function fn(){
? ? ? var c=20;
? ? ? var d=100;
? }
? fn();
}
foo();
在這個栗子中呕诉,包括了三個執(zhí)行環(huán)境:全局環(huán)境,foo()執(zhí)行環(huán)境吃度,fn()執(zhí)行環(huán)境甩挫;
執(zhí)行環(huán)境的處理機(jī)制
在這里我們要了解到執(zhí)行上下文的第一個特點(diǎn):內(nèi)部的環(huán)境可以訪問外部的環(huán)境,而外部的環(huán)境無法訪問內(nèi)部的環(huán)境椿每。
例如:我們可以在fn()中訪問到位于foo()中的b伊者,在全局環(huán)境中的a,而在foo()中卻無法訪問到c或者d间护。
為什么會這樣亦渗,這就要了解JS處理代碼的一個機(jī)制了。
我們知道JS的處理過程是以堆棧的方式來處理兑牡,JS引擎會把執(zhí)行環(huán)境一個個放入棧里央碟,然后先放進(jìn)去的后處理,后放進(jìn)去的先處理均函,上面這個栗子亿虽,最先被放進(jìn)棧中的是全局環(huán)境,然后是foo()苞也,再是fn()洛勉,然后處理完一個拿出一個來,所以我們知道為什么foo()不能訪問fn()里的了如迟,因?yàn)樗呀?jīng)走了收毫。
執(zhí)行環(huán)境的生命周期
好了,了解完執(zhí)行環(huán)境的的處理方式殷勘,我們要說明執(zhí)行環(huán)境的生命周期此再。執(zhí)行環(huán)境的生命周期分為兩個階段,這兩個階段描述了執(zhí)行環(huán)境在棧里面做了些什么玲销。
創(chuàng)建階段输拇;執(zhí)行階段
創(chuàng)建階段
執(zhí)行環(huán)境在創(chuàng)建階段會完成這么幾個任務(wù):1.生成變量對象;2.建立作用域鏈贤斜;3.確定this指向
執(zhí)行階段
到了執(zhí)行階段策吠,會給變量賦值,函數(shù)引用瘩绒,然后還有執(zhí)行其他的代碼猴抹。
完成了這兩個步驟,執(zhí)行環(huán)境就可以準(zhǔn)備出棧锁荔,一路走好了蟀给。
以上就是執(zhí)行環(huán)境的具體執(zhí)行內(nèi)容。上面提到了執(zhí)行環(huán)境在創(chuàng)建階段會生成變量對象,這也是一個很重要的概念坤溃,我們下文會詳細(xì)論述拍霜。
變量對象(variable object)
變量對象是什么呢?《JS高程》是這樣說的:“每個執(zhí)行環(huán)境都有與之關(guān)聯(lián)的變量對象薪介,環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中≡阶ぃ”
那變量對象里有些什么東西呢汁政?看下文:
變量對象的內(nèi)容
在變量對象創(chuàng)建時,經(jīng)過了這樣三個步驟:
生成arguments屬性缀旁;找到function函數(shù)聲明记劈,創(chuàng)建屬性;找到var變量聲明并巍,創(chuàng)建屬性
其中值得注意的是:function函數(shù)聲明的級別比var變量聲明的級別要高目木,所以在實(shí)際執(zhí)行的過程中會先尋找function的聲明。
還需要注意的是:在執(zhí)行環(huán)境的執(zhí)行階段之前懊渡,變量對象中的屬性都無法訪問刽射,這里還有一個活動對象(activation object)的概念,其實(shí)這個概念正是由進(jìn)入執(zhí)行階段的變量對象轉(zhuǎn)化而來剃执。
來看一個栗子:
function foo(){
? ? var a=10;
? function fn(){
? return5;
? }?
}
foo();
讓我們來看看foo()函數(shù)的執(zhí)行環(huán)境:
它會包括三個部分:1.變量對象誓禁;2.作用域鏈;3.this指向?qū)ο?/p>
創(chuàng)建階段:
建立arguments找到fn()肾档;找到變量a摹恰,undefined;
執(zhí)行階段:
變量對象變成活動對象怒见;arguments還是它~fn();a=10;
以上就是變量對象的內(nèi)容了俗慈,需要記住這個東西,因?yàn)闀奖阄覀兞私庀挛牧硪粋€重要的概念:作用域鏈遣耍。
作用域鏈(scope chain)
什么是作用域鏈闺阱?《JS高程》里的文字是:“作用域鏈的用途,是保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問配阵×笏蹋”懵不懵逼?反正我第一次看到的時候確實(shí)是懵逼了棋傍。前面我們說過作用域救拉,那么作用域鏈?zhǔn)遣皇蔷褪谴谝黄鸬淖饔糜蚰兀坎⒉皇恰?/p>
作用域和作用域鏈的關(guān)系瘫拣,作用域是一套通過標(biāo)識符查找變量的規(guī)則亿絮。而作用域鏈則是這套規(guī)則這套規(guī)則的具體運(yùn)行。
是不是還是有點(diǎn)懵逼?還是看例子吧:
function foo(){
? ? ?var a=10;
? function fn(){
? ? return5;
? }
}
foo();
我們還是用上面的栗子派昧,這次我們只看作用域鏈黔姜,根據(jù)規(guī)則,在一個函數(shù)的執(zhí)行環(huán)境的作用域鏈上蒂萎,會依次放入自己的變量對象秆吵,父級的變量對象,祖級的變量對象…一直到全局的變量對象五慈。
比如上面這個栗子纳寂,fn()的執(zhí)行環(huán)境的作用域鏈上會有些什么呢?首先是自己的OV泻拦,然后是foo()的OV毙芜,接著就是全局的OV。而foo()的作用域鏈則會少一個fn()的OV争拐。(OV是變量對象的縮寫)
那這樣放有什么好處呢腋粥?我們知道“作用域鏈保證了當(dāng)前執(zhí)行環(huán)境對符合訪問權(quán)限的變量和函數(shù)的有序訪問〖懿埽”有序隘冲!外層函數(shù)不能訪問內(nèi)層函數(shù)的變量,而內(nèi)層能夠訪問外層音瓷。正是有了這個作用域鏈对嚼,通過這個有方向的鏈,我們可以查找標(biāo)識符绳慎,進(jìn)而找到變量纵竖,才能實(shí)現(xiàn)這個特性。
閉包
好了杏愤,終于要講到這個前端小萌新眼里的小boss了靡砌。在技術(shù)博客和書里翻滾了將將一周,對閉包的各種解釋把我搞得精力憔悴珊楼,懷疑人生通殃。以至于在寫下這段關(guān)于閉包的論述時,也是內(nèi)心忐忑厕宗,因?yàn)槲乙膊淮_定我說的是百分之百正確画舌。
先看看《JS高程》說的:“閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)∫崖”
說法是:“當(dāng)函數(shù)可以記住并訪問所在的作用域(全局作用域除外)時曲聂,就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前作用域之外執(zhí)行佑惠∨笠福”
…
好吧其實(shí)我覺得都說的不是太清楚齐疙。讓我們這樣來理解,就是內(nèi)部函數(shù)引用了外部函數(shù)的變量對象時旭咽,外部函數(shù)就是一個閉包贞奋。
還是看例子吧。
function foo(){
? ? var a=20;
? ? return
? ? function(){
? ? ? return a;
? ? }
}
foo()();
在這個栗子中穷绵,foo()函數(shù)內(nèi)部返回了一個匿名函數(shù)轿塔,而匿名函數(shù)內(nèi)部引用了外部函數(shù)foo()的變量a,由于作用域鏈请垛,這個引用是有效的催训,按照J(rèn)S的機(jī)制,foo()執(zhí)行完畢后宗收,執(zhí)行環(huán)境會失去引用,內(nèi)存會銷毀亚兄,但是由于內(nèi)部的匿名函數(shù)的引用混稽,a會被暫時保存下來,罩著a的就是閉包审胚。
return一個匿名函數(shù)時創(chuàng)造一個閉包的最簡單的方式匈勋,實(shí)際上創(chuàng)造閉包十分靈活,再看一個栗子:
var fn =null;function foo(){
? ? var a =2;
? ? function innnerFoo(){
? ? ? console.log(a);
? }
? ?fn = innnerFoo;
}
function bar(){
? fn();
}
foo();
bar();// 2
如上膳叨,可以看到:通過把innnerFoo()賦值給全局變量fn洽洁,內(nèi)部的函數(shù)在當(dāng)前作用域外執(zhí)行了,但是這不會影響foo形成了一個閉包菲嘴。
閉包和兩個不同的案例
這兩組栗子都是在各種書籍和各種博客上司空見慣了的栗子饿自,其實(shí)跟閉包的關(guān)系不是很大,但是涉及到了函數(shù)相關(guān)的知識點(diǎn)龄坪,所以在這里寫下來昭雌。也算是積累。
閉包和變量(見《JS高程》P181)
一個例子
function createFunction(){
? ? var result=newArray();
? ? for(i=0;i<10;i++){
? ? ? result[i]=function(){
? ? ? return i;
? ? ?}
?}
return result;
}
alert(createFunction());
這個例子并不會如我們以為的返回從0到9的一串索引值健田。當(dāng)我們執(zhí)行createFunction()時烛卧,函數(shù)內(nèi)會return result,而我們注意到result是一個數(shù)組妓局,而每一個result[i]呢总放?它返回的則是一個函數(shù),而不是這個函數(shù)的執(zhí)行結(jié)果 i好爬。
所以我們想要返回一串索引值的時候局雄,試著選擇result數(shù)組的其中一個,再加上圓括號讓它執(zhí)行起來抵拘,像這樣:
createFunction()[2]()
這樣子就能執(zhí)行了嗎哎榴?運(yùn)行起來發(fā)現(xiàn)并沒有型豁,執(zhí)行的結(jié)果是一串的i,為什么呢尚蝌?
原因是在執(zhí)行createFunction()的時候迎变,i的值已經(jīng)增加到了10,即退出循環(huán)的值飘言,而再要執(zhí)行result內(nèi)部的匿名函數(shù)時衣形,它能獲取到的i就只有10了,所以不管引用多少次姿鸿,i的值都會是10谆吴;
那要如何修改才能達(dá)到我們的目的呢?
function createFunction(){
? ? ? var result=[];
? ? ? for(i=0;i<10;i++){
? ? ? result[i]=function(num){
? ? ? ? returnfunction(){
? ? ? ? return num;
? ? ?};
? }(i);
}
return result;
}
alert(createFunction()[2]());
彈出的警告和索引值一模一樣苛预。這又是什么原因呢句狼?
我們執(zhí)行
createFunction()
時,把外部的匿名函數(shù)的執(zhí)行結(jié)果賦值給了result热某,返回的result就是十個函數(shù)的數(shù)組腻菇。
而在這個外部函數(shù)里,有一個參數(shù)num昔馋,由于IIFE(立即執(zhí)行函數(shù))的緣故筹吐,循環(huán)過程中的i被賦值給了一個個的num,前后一共保存了10個num秘遏,為什么能夠保存下來呢丘薛?因?yàn)閮?nèi)部的匿名函數(shù)引用了num。而這外部函數(shù)就是一個閉包
接下來邦危,當(dāng)執(zhí)行
createFunction()[2]()
時實(shí)際上是執(zhí)行這個數(shù)組result的第三項(xiàng)洋侨,即:
function(){
? ?return num;
};
這個函數(shù)。
num值是多少呢铡俐?如前所述凰兑,正是對應(yīng)的i。所以返回的值就能夠達(dá)到我們的預(yù)期了审丘。
實(shí)際上吏够,我認(rèn)為這個例子中更重要的是自執(zhí)行函數(shù)這個概念,正是有了自執(zhí)行滩报,才能形成多對對多的引用锅知,盡管這個例子里確實(shí)存在閉包,不過我認(rèn)為用這個例子來介紹閉包并不是太恰當(dāng)脓钾。
閉包和this
this也是JS里一個重中之重售睹。我們知道,JS的this十分靈活的可训,前面已經(jīng)介紹過昌妹,this的指向在函數(shù)執(zhí)行環(huán)境建立時確定捶枢。函數(shù)中的this的指向是一個萌新們的難點(diǎn),什么時候它是指向全局環(huán)境呢飞崖?什么時候它又是指向?qū)ο竽乩檬澹孔⒁猓捍颂幱懻摰氖侵负瘮?shù)中的this,全局環(huán)境下的this一般情況指向window固歪。
結(jié)論一:this的指向是在函數(shù)被調(diào)用的時候確定的
因?yàn)楫?dāng)一個函數(shù)調(diào)用時蒜鸡,一個執(zhí)行環(huán)境就創(chuàng)建了,接著它會執(zhí)行牢裳,這是執(zhí)行環(huán)境的生命周期逢防。所以this的指向是在函數(shù)被調(diào)用時確定的。
結(jié)論二:當(dāng)函數(shù)執(zhí)行時蒲讯,如果這個函數(shù)是屬于某個對象忘朝,調(diào)用的方式是以對象的方法進(jìn)行的,那么this的指向就是這個對象判帮,而其他情況辜伟,如函數(shù)獨(dú)立調(diào)用,則基本是指向全局對象脊另。
PS:實(shí)際上這個說法不大準(zhǔn)確,當(dāng)函數(shù)獨(dú)立調(diào)用時约巷,在嚴(yán)格模式下偎痛,this的指向時undefined,而非嚴(yán)格模式下独郎,則時指向全局對象踩麦。
為了更好的說明,讓我們看一個例子:
var a =20;
var foo ={
? a:10,
? getA:function(){
? ? returnthis.a;
? }
}
console.log(foo.getA());// 10
var test = foo.getA;
console.log(test());// 20
在上面這個例子中氓癌,foo.getA()作為對象方法的調(diào)用谓谦,指向的自然是這個對象,而test雖然指向和foo.getA相同贪婉,但是因?yàn)槭仟?dú)立調(diào)用反粥,所以在非嚴(yán)格模式下,指向的是全局對象疲迂。
除了上面的例子才顿,在《JS高程》中還有一個經(jīng)典的例子,眾多博客文章均有討論尤蒿,但是看過之后覺得解釋還是不夠清楚郑气,至少我沒完全理解,這里我將試著用自己的語言來解釋腰池。
var name="the window";
varobject={
? name:"my object",
? getNameFunc:function(){
? ? returnfunction(){
? ? ? returnthis.name;
? ? };
? }
};
alert(object.getNameFunc()());// the window
在這個帶有閉包的例子里尾组,我們可以看到object.getNameFunc()執(zhí)行的返回是一個函數(shù)忙芒,再加()執(zhí)行則是一個直接調(diào)用了。所以指向的是全局對象讳侨。
如果我們想要返回變量對象怎么辦呢呵萨?
讓我們看一段代碼:
var name=“the window”;
varobject={
? name:"my object",getFunc:function(){
? ? returnthis.name;
}};
alert(object.getFunc());//"my object"```
我去掉了上面例子的閉包,可以看出在方法調(diào)用的情況下爷耀,this指向的是對象甘桑,那么我們只要在閉包能訪問到的位置,同時也是在這個方法調(diào)用的同一個作用域里設(shè)置一個“中轉(zhuǎn)站”就好了歹叮,讓我們把這個位置的this賦值給一個變量來存儲跑杭,然后匿名函數(shù)調(diào)用這個變量時指向的就會是對象而不是全局對象了。
var name="the window";
varobject={
? name:"my object",
? ? getFunc:function(){
? ? var that=this;
? ? ? returnfunction(){
? ? ? ? return that;
? ? };
? }
};
alert(object.getFunc());
that’s all
閉包的應(yīng)用
閉包的應(yīng)用太多了咆耿,最重要的一個就是模塊模式了德谅。不過說實(shí)話,實(shí)在還沒上路萨螺,所以這里就用一個模塊的栗子來結(jié)尾吧窄做。(強(qiáng)行結(jié)尾)
(function(){
? ? var a =10;
? ? var b =20;
? ? ? function add(num1, num2){
? ? ? ? var num1 =!!num1 ? num1 : a;
? ? ? ?var num2 =!!num2 ? num2 : b;
? ? ? return num1 + num2;
? ? }
? ? window.add = add;
})();
add(10,20);
我們需要知道的是,所謂模塊利用的就是閉包外部無法訪問內(nèi)部慰技,內(nèi)部卻能訪問外部的特性椭盏,通過引用了指定的公共變量和方法,達(dá)到訪問私有變量和方法的目的吻商。模塊可以保證模塊內(nèi)部的私有方法和變量不被外部變量污染掏颊,進(jìn)而方便更大規(guī)模的開發(fā)項(xiàng)目。