摘要:為了防止之后自己又開始模糊,所以自己來總結(jié)一下中關(guān)于作用域鏈和原型鏈的知識(shí),并將二者相比較看待進(jìn)一步加深理解。因此我們發(fā)現(xiàn)當(dāng)多個(gè)作用域相互嵌套的時(shí)候,就形成了作用域鏈。原型鏈原型說完了作用域鏈,我們來講講原型鏈。
畢業(yè)也整整一年了,看著很多學(xué)弟都畢業(yè)了,忽然心中頗有感慨,時(shí)間一去不復(fù)還呀。記得從去年這個(gè)時(shí)候接觸到JavaScript,從一開始就很喜歡這門語(yǔ)言,當(dāng)時(shí)迷迷糊糊看完了《JavaScript高級(jí)程序設(shè)計(jì)》這本書,似懂非懂。這幾天又再次回顧了這本書,之前很多不理解的內(nèi)容似乎開始有些豁然開朗了。為了防止之后自己又開始模糊,所以自己來總結(jié)一下JavaScript中關(guān)于 作用域鏈和原型鏈的知識(shí),并將二者相比較看待進(jìn)一步加深理解。以下內(nèi)容都純屬于自己的理解,有不對(duì)的地方歡迎指正。
作用域鏈 作用域首先我們需要了解的是作用域做什么的?當(dāng)JavaScript引擎在某一作用域中遇見變量和函數(shù)的時(shí)候,需要能夠明確變量和函數(shù)所對(duì)應(yīng)的值是什么,所以就需要作用域來對(duì)變量和函數(shù)進(jìn)行查找,并且還需要確定當(dāng)前代碼是否對(duì)該變量具有訪問權(quán)限。也就是說作用域主要有以下的任務(wù):
收集并維護(hù)所有聲明的標(biāo)識(shí)符(變量和函數(shù))
依照特定的規(guī)則對(duì)標(biāo)識(shí)符進(jìn)行查找
確定當(dāng)前的代碼對(duì)標(biāo)識(shí)符的訪問權(quán)限
舉一個(gè)例子:
function foo(a) { console.log( a ); // 2 } foo( 2 );
對(duì)于上述代碼,JavaScript引擎需要對(duì)作用域發(fā)出以下的命令
查詢標(biāo)識(shí)符foo,得到變量后執(zhí)行該變量
查詢標(biāo)識(shí)符a,得到變量后對(duì)其賦值為2
查詢標(biāo)識(shí)符console,得到變量后準(zhǔn)備執(zhí)行屬性log
查詢標(biāo)識(shí)符a,得到變量后,作為參數(shù)傳入console.log執(zhí)行
我們省略了函數(shù)console.log內(nèi)部的執(zhí)行過程,我們可以看到對(duì)JavaScript引擎來說,作用域最重要的功能就是查詢標(biāo)識(shí)符。從上面的例子來看,引擎對(duì)變量的使用其實(shí)不是都一樣的。比如第一步引擎得到標(biāo)識(shí)符foo的目的是執(zhí)行它(或者說是為了拿到標(biāo)識(shí)符里存儲(chǔ)的值)。
但第二步中引擎查找標(biāo)識(shí)符a的目的是為了對(duì)其賦值(也就是改變存儲(chǔ)的值)。所以查找也分為兩種:LHS和RHS。
我在之前的一篇文章中從LHS與RHS角度淺談Js變量聲明與賦值曾經(jīng)介紹過LHS與RHS,這兩個(gè)看起來很高大上的名詞其實(shí)非常簡(jiǎn)單。LHS指的是Left-hand Side,而RHS指的是Right-hand Side。分別對(duì)應(yīng)于兩種不同目的的詞法查詢。LHS所查詢的目的是為了賦值(類似于該變量會(huì)位于賦值符號(hào)=的左邊),例如第二步查找變量a的過程。而RHS所查詢的目的是為了引用(類似于變量會(huì)位于賦值符號(hào)=的右邊),例如第一步查找變量foo的過程。
我們知道代碼不僅僅可以訪問當(dāng)前的作用域的變量,對(duì)于嵌套的父級(jí)作用域中的變量也可以訪問。我們先只在ES5中表述,我們知道JavaScript在ES5中是沒有塊級(jí)作用域的,只有函數(shù)可以創(chuàng)建作用域。舉個(gè)例子:
function Outer(){ var outer = "outer"; Inner(); function Inner(){ var inner = "inner"; console.log(outer,inner) // outer inner } }
當(dāng)引擎執(zhí)行到函數(shù)Inner內(nèi)部的時(shí)候,不僅可以訪問當(dāng)前作用域而且可以訪問到Outer的作用域,從而可以訪問到標(biāo)識(shí)符outer。因此我們發(fā)現(xiàn)當(dāng)多個(gè)作用域相互嵌套的時(shí)候,就形成了作用域鏈。詞法作用域在查找標(biāo)識(shí)符的時(shí)候,優(yōu)先在本作用域中查找。如果在本作用域沒有找到標(biāo)識(shí)符,會(huì)繼續(xù)向上一級(jí)查找,當(dāng)?shù)诌_(dá)最外層的全局作用域仍然沒有找到,則會(huì)停止對(duì)標(biāo)識(shí)符的搜索。如果沒有查找到標(biāo)識(shí)符,會(huì)根據(jù)不同的查找方式作出不同的反應(yīng)。如果是RHS,則會(huì)拋出Uncaught ReferenceError的錯(cuò)誤,如果是LHS,則會(huì)在查找最外層的作用域聲明該變量,這就解釋了為什么對(duì)未聲明的變量賦值后該變量會(huì)成為全局變量。所以上面的代碼執(zhí)行
console.log(outer,inner)
的時(shí)候,引擎會(huì)首先要求Inner函數(shù)的詞法作用域查找(RHS)標(biāo)識(shí)符outer,被告知該詞法作用域不存在該標(biāo)識(shí)符,然后引擎會(huì)要求嵌套的上一級(jí)Outer詞法作用域查找(RHS)標(biāo)識(shí)符outer,Outer詞法作用域的查找成功并將結(jié)果返回給引擎。
換個(gè)角度理解作用域鏈 上面我們理解作用域鏈都是從作用域鏈查找變量的角度去考慮的,其實(shí)這已經(jīng)足夠了,大部分作用域鏈的場(chǎng)景都是查找標(biāo)識(shí)符。但是我們可以換一個(gè)角度去理解作用域鏈。其實(shí)JavaScript的每個(gè)函數(shù)都有對(duì)應(yīng)的執(zhí)行環(huán)境(execution context)。當(dāng)執(zhí)行流進(jìn)入進(jìn)入一個(gè)函數(shù)時(shí),該函數(shù)的執(zhí)行環(huán)境就會(huì)被推入環(huán)境棧,當(dāng)函數(shù)執(zhí)行結(jié)束之后,該函數(shù)的執(zhí)行環(huán)境就會(huì)被彈出環(huán)境棧,執(zhí)行環(huán)境被變更為之前的執(zhí)行環(huán)境。而每創(chuàng)建一個(gè)執(zhí)行環(huán)境時(shí),會(huì)同時(shí)生成一個(gè)變量對(duì)象(variable object)(函數(shù)生成的是活動(dòng)變量(activation object)),用來存儲(chǔ)當(dāng)前執(zhí)行環(huán)境中定義的變量和函數(shù),當(dāng)執(zhí)行環(huán)境結(jié)束時(shí),當(dāng)前的變量(活動(dòng))對(duì)象就會(huì)被銷毀(全局的變量對(duì)象是一直存在的,不會(huì)被銷毀)。雖然我們無(wú)法訪問到變量(活動(dòng))對(duì)象,但詞法作用域查找標(biāo)識(shí)符會(huì)使用它。
當(dāng)對(duì)于函數(shù)的執(zhí)行環(huán)境生成的活動(dòng)對(duì)象,初始化就會(huì)存在兩個(gè)變量:this和arguments,因此我們?cè)诤瘮?shù)中就直接可以使用這兩個(gè)變量。對(duì)于作用域鏈存儲(chǔ)都是變量(活動(dòng))對(duì)象,而當(dāng)前執(zhí)行環(huán)境的變量對(duì)象就存儲(chǔ)在作用域鏈的最前端,優(yōu)先被查找。從這個(gè)角度看,標(biāo)識(shí)符解析是沿著作用域鏈一級(jí)一級(jí)地在變量(活動(dòng))對(duì)象中搜索標(biāo)識(shí)符的過程。搜索過程始終從作用域鏈的前端開始,然后逐級(jí)地向后回溯,直至找到標(biāo)識(shí)符為止。
這年頭出去面試JavaScript的崗位,各個(gè)都要問你閉包的問題,開始的時(shí)候覺得閉包的概念蠻高級(jí)的,后來覺得這個(gè)也沒啥東西可講的。老早的之前就寫過一篇關(guān)于閉包的文章淺談JavaScript閉包,講到現(xiàn)在我覺得把閉包放到作用域鏈一起將會(huì)更好。還是繼續(xù)講個(gè)例子:
function fn(){ var a = "JavaScript"; function func(){ console.log(a); } return func; } var func = fn(); func(); //JavaScript
首先明確一下什么是閉包?我認(rèn)為閉包最好的概念解釋就是:
函數(shù)在定義的詞法作用域以外的地方被調(diào)用,閉包使得函數(shù)可以繼續(xù)訪問定義時(shí)的詞法作用域。
func函數(shù)執(zhí)行的位置和定義的位置是不相同的,func是在函數(shù)fn中定義的,但執(zhí)行卻是在全局環(huán)境中,雖然是在全局函數(shù)中執(zhí)行的,但函數(shù)仍然可以訪問當(dāng)定義時(shí)的詞法作用域。如下圖所示:
我們之前說過,當(dāng)函數(shù)執(zhí)行結(jié)束后其活動(dòng)變量就會(huì)被銷毀,但是在上面的例子中卻不是這個(gè)樣子。但函數(shù)fn執(zhí)行結(jié)束之后,fn對(duì)象的活動(dòng)變量并沒有被銷毀,這是因?yàn)?b>fn返回的函數(shù)func的作用域鏈還保持著fn的活動(dòng)變量,因此JavaScript的垃圾回收機(jī)制不會(huì)回收fn活動(dòng)變量。雖然返回的函數(shù)func是在全局環(huán)境下執(zhí)行的,但是其作用域鏈的存儲(chǔ)的活動(dòng)(變量)對(duì)象的順序分別是:func的活動(dòng)變量、fn的活動(dòng)變量、全局變量對(duì)象。因此在func函數(shù)執(zhí)行時(shí),會(huì)順著作用域鏈查找標(biāo)識(shí)符,也就能訪問到fn所定義的詞法作用域(即fn函數(shù)的活動(dòng)變量)也就不足為奇了。這樣看起來是不是覺得閉包也是非常的簡(jiǎn)單。
說完了作用域鏈,我們來講講原型鏈。首先也是要明確什么是原型?所有的函數(shù)都有一個(gè)特殊的屬性: prototype(原型),prototype屬性是一個(gè)指針,指向的是一個(gè)對(duì)象(原型對(duì)象),原型對(duì)象中的方法和屬性都可以被函數(shù)的實(shí)例所共享。所謂的函數(shù)實(shí)例是指以函數(shù)作為構(gòu)造函數(shù)創(chuàng)建的對(duì)象,這些對(duì)象實(shí)例都可以共享構(gòu)造函數(shù)的原型的方法。舉個(gè)例子:
var Person = function(name){ this.name = name; } Person.prototype.sayName = function(){ console.log("name: ",this.name) }; var person = new Person("JavaScript"); person.sayName(); //JavaScript
在上面的例子中,對(duì)象person是構(gòu)造函數(shù)Person創(chuàng)建的實(shí)例。所謂的構(gòu)造函數(shù)也只不過是普通的函數(shù)通過操作符new來調(diào)用。在使用new操作符調(diào)用函數(shù)時(shí)主要執(zhí)行以下幾個(gè)步驟:
創(chuàng)建新的對(duì)象,并將函數(shù)的this指向新創(chuàng)建的對(duì)象
執(zhí)行函數(shù)
返回新創(chuàng)建的對(duì)象
通過構(gòu)造函數(shù)返回的對(duì)象,其中含有一個(gè)內(nèi)部指針[[Prototype]]指向構(gòu)造函數(shù)的原型對(duì)象,當(dāng)然我們是無(wú)法訪問到這個(gè)標(biāo)準(zhǔn)的內(nèi)部指針[[Prototype]],但是在Firefox、Safari和Chrome在上都支持一個(gè)屬性__proto__,用來指向構(gòu)造函數(shù)的原型對(duì)象。下圖就解釋了上面的結(jié)構(gòu):
我們可以看到,構(gòu)造函數(shù)Person的prototype屬性指向Prototype的原型對(duì)象。而person作為構(gòu)造函數(shù)Person創(chuàng)建的實(shí)例,其中存在內(nèi)部指針也指向Person的原型對(duì)象。需要注意的是,在Person的原型對(duì)象中存在一個(gè)特殊的屬性constructor,指向構(gòu)造函數(shù)Person。在我們的例子中,執(zhí)行到:
person.sayName(); //JavaScript
當(dāng)執(zhí)行person的sayName屬性時(shí),首先會(huì)在對(duì)象實(shí)例中查找sayName屬性,當(dāng)發(fā)現(xiàn)對(duì)象實(shí)例中不存在sayName時(shí),會(huì)轉(zhuǎn)而去搜索person內(nèi)部指針[[Prototpe]]所指向的原型對(duì)象,當(dāng)發(fā)現(xiàn)原型對(duì)象中存在sayName屬性時(shí),執(zhí)行該屬性。關(guān)于函數(shù)sayName中this的指向,有興趣可以戳這篇文章一個(gè)小小的JavaScript題目。
講完了原型,再講講原型鏈,其實(shí)我們上面的圖并不完整,因?yàn)?strong>所有函數(shù)的默認(rèn)原型都是Object的實(shí)例,所以函數(shù)原型實(shí)例的內(nèi)部指針[[Prototype]]指向的是Object.prototype,讓我們繼續(xù)來完善一下:
這就是完整的原型鏈,假如我們執(zhí)行下面代碼:
person.toString()
執(zhí)行上面代碼時(shí),首先會(huì)在對(duì)象實(shí)例person中查找屬性toString方法,我們發(fā)現(xiàn)實(shí)例中不存在toString屬性。然后我們轉(zhuǎn)到person內(nèi)部指針[[Prototype]]指向的Person原型對(duì)象去尋找toString屬性,結(jié)果是仍然不存在。這找不到我們就放棄了?開玩笑,我們這么有毅力。我們會(huì)再接著到Person原型對(duì)象的內(nèi)部指針[[Prototype]]指向的Object原型對(duì)象中查找,這次我們發(fā)現(xiàn)其中確實(shí)存在toString屬性,然后我們執(zhí)行toString方法。發(fā)現(xiàn)了沒有,這一連串的原型形成了一條鏈,這就是原型鏈。
其實(shí)我們上面例子中對(duì)屬性toString查找屬于RHS,以RHS方式尋找屬性時(shí),會(huì)在原型鏈中依次查找,如果在當(dāng)前的原型中已經(jīng)查找到所需要的屬性,那么就會(huì)停止搜索,否則會(huì)一直向后查找原型鏈,直到原型鏈的結(jié)尾(這一點(diǎn)有點(diǎn)類似于作用域鏈),如果直到原型鏈結(jié)尾仍未找到,那么該屬性就是undefined。但執(zhí)行LHS方式的查找卻截然不同,當(dāng)發(fā)現(xiàn)對(duì)象實(shí)例本身不存在該屬性,直接在該對(duì)象實(shí)例中聲明變量,而不會(huì)去查找原型鏈。例如:
person.toString = function(){ console.log("person") } person.toString(); //person
當(dāng)對(duì)person執(zhí)行LHS的方式查找toString屬性時(shí),我們發(fā)現(xiàn)person中并不存在toString,這時(shí)會(huì)直接在person中聲明屬性,而不會(huì)去查找原型鏈,接著我們執(zhí)行person.toString()時(shí),我們?cè)趯?shí)例中找到了toString屬性并將其執(zhí)行,這樣實(shí)例中的toString就屏蔽了原型鏈中的toString屬性。
講完了作用域鏈和原型鏈,我們可以比較一下。作用域鏈的作用主要用于查找標(biāo)識(shí)符,當(dāng)作用域需要查詢變量的時(shí)候會(huì)沿著作用域鏈依次查找,如果找到標(biāo)識(shí)符就會(huì)停止搜索,否則將會(huì)沿著作用域鏈依次向后查找,直到作用域鏈的結(jié)尾。而原型鏈?zhǔn)怯糜?strong>查找引用類型的屬性,查找屬性會(huì)沿著原型鏈依次進(jìn)行,如果找到該屬性會(huì)停止搜索并做相應(yīng)的操作,否則將會(huì)沿著原型鏈依次查找直到結(jié)尾。
如果覺得閱讀完了本篇文章對(duì)你有些許幫助,歡迎大家我關(guān)注我的掘金賬號(hào)或者star我的Github的blog項(xiàng)目,也算是對(duì)我的鼓勵(lì)啦!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/83709.html
摘要:變量對(duì)象也是有父作用域的。作用域鏈的頂端是全局對(duì)象。當(dāng)函數(shù)被調(diào)用的時(shí)候,作用域鏈就會(huì)包含多個(gè)作用域?qū)ο?。?dāng)函數(shù)要訪問時(shí),沒有找到,于是沿著作用域鏈向上查找,在的作用域找到了對(duì)應(yīng)的標(biāo)示符,就會(huì)修改的值。 一、概要 對(duì)于閉包的定義(紅寶書P178):閉包就是指有權(quán)訪問另外一個(gè)函數(shù)的作用域中的變量的函數(shù)。 關(guān)鍵點(diǎn): 1、閉包是一個(gè)函數(shù) 2、能夠訪問另外一個(gè)函數(shù)作用域中的變量 二、閉包特性 對(duì)...
摘要:前端日?qǐng)?bào)精選如何在非項(xiàng)目中使用知乎專欄編碼規(guī)范最常被遺忘的性能優(yōu)化瀏覽器緩存?zhèn)€人文章譯統(tǒng)一樣式語(yǔ)言掘金新的開發(fā)者提及最多的個(gè)視頻眾成翻譯中文第期在中使用譯統(tǒng)一樣式語(yǔ)言掘金前端現(xiàn)狀答題救不了前端新人相學(xué)長(zhǎng)懟前端歲以 2017-06-29 前端日?qǐng)?bào) 精選 如何在非 React 項(xiàng)目中使用 Redux - 知乎專欄Javascript編碼規(guī)范 - Clearlove - SegmentFau...
摘要:本期推薦文章從作用域鏈談閉包,由于微信不能訪問外鏈,點(diǎn)擊閱讀原文就可以啦。推薦理由這是一篇譯文,深入淺出圖解作用域鏈,一步步深入介紹閉包。作用域鏈的頂端是全局對(duì)象,在全局環(huán)境中定義的變量就會(huì)綁定到全局對(duì)象中。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周開始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第6天。 本...
摘要:這幾天因?yàn)閷?duì)于中的作用域鏈和原型鏈有點(diǎn)混淆,當(dāng)訪問一個(gè)不帶有修飾的變量時(shí),我想知道它的搜索順序,因?yàn)樽饔糜蜴湹逆溄Y(jié)點(diǎn)也是一個(gè)變量對(duì)象,那么當(dāng)在這個(gè)變量對(duì)象中查找變量時(shí)會(huì)不會(huì)沿著它的原型鏈查找呢這樣就有兩種可能先查找作用域鏈前端的變量對(duì)象,然 這幾天因?yàn)閷?duì)于JavaScript中的作用域鏈和原型鏈有點(diǎn)混淆,當(dāng)訪問一個(gè)不帶有this修飾的變量時(shí),我想知道它的搜索順序,因?yàn)樽饔糜蜴湹逆溄Y(jié)點(diǎn)也...
摘要:在之前我們根絕對(duì)象的原型說過了的原型鏈,那么同樣的萬(wàn)物皆對(duì)象,函數(shù)也同樣存在這么一個(gè)鏈?zhǔn)降年P(guān)系,就是函數(shù)的作用域鏈作用域鏈?zhǔn)紫认葋砘仡櫼幌轮爸v到的原型鏈的尋找機(jī)制,就是實(shí)例會(huì)先從本身開始找,沒有的話會(huì)一級(jí)一級(jí)的網(wǎng)上翻,直到頂端沒有就會(huì)報(bào)一 在之前我們根絕對(duì)象的原型說過了js的原型鏈,那么同樣的js 萬(wàn)物皆對(duì)象,函數(shù)也同樣存在這么一個(gè)鏈?zhǔn)降年P(guān)系,就是函數(shù)的作用域鏈 作用域鏈 首先先來回...
閱讀 1278·2021-09-02 13:36
閱讀 2730·2019-08-30 15:44
閱讀 2986·2019-08-29 15:04
閱讀 3204·2019-08-26 13:40
閱讀 3652·2019-08-26 13:37
閱讀 1184·2019-08-26 12:22
閱讀 1030·2019-08-26 11:36
閱讀 1227·2019-08-26 10:41