畢業也整整一年了,看着不少學弟都畢業了,突然心中很有感慨,時間一去不復還呀。記得從去年這個時候接觸到JavaScript,從一開始就很喜歡這門語言,當時迷迷糊糊看完了《JavaScript高級程序設計》這本書,似懂非懂。這幾天又再次回顧了這本書,以前不少不理解的內容彷佛開始有些豁然開朗了。爲了防止以後本身又開始模糊,因此本身來總結一下JavaScript中關於 做用域鏈和原型鏈的知識,並將兩者相比較看待進一步加深理解。如下內容都純屬於本身的理解,有不對的地方歡迎指正。javascript
首先咱們須要瞭解的是做用域作什麼的?當JavaScript引擎在某一做用域中碰見變量和函數的時候,須要可以明確變量和函數所對應的值是什麼,因此就須要做用域來對變量和函數進行查找,而且還須要肯定當前代碼是否對該變量具備訪問權限。也就是說做用域主要有如下的任務:前端
舉一個例子:java
function foo(a) {
console.log( a ); // 2
}
foo( 2 );複製代碼
對於上述代碼,JavaScript引擎須要對做用域發出如下的命令git
foo
,獲得變量後執行該變量a
,獲得變量後對其賦值爲2console
,獲得變量後準備執行屬性log
a
,獲得變量後,做爲參數傳入console.log
執行 咱們省略了函數console.log
內部的執行過程,咱們能夠看到對JavaScript引擎來講,做用域最重要的功能就是查詢標識符。從上面的例子來看,引擎對變量的使用其實不是都同樣的。好比第一步引擎獲得標識符foo
的目的是執行它(或者說是爲了拿到標識符裏存儲的值)。
但第二步中引擎查找標識符a
的目的是爲了對其賦值(也就是改變存儲的值)。因此查找也分爲兩種:LHS
和RHS
。github
我在以前的一篇文章中從LHS與RHS角度淺談Js變量聲明與賦值曾經介紹過LHS
與RHS
,這兩個看起來很高大上的名詞其實很是簡單。LHS
指的是Left-hand Side
,而RHS
指的是Right-hand Side
。分別對應於兩種不一樣目的的詞法查詢。LHS
所查詢的目的是爲了賦值(相似於該變量會位於賦值符號=
的左邊),例如第二步查找變量a
的過程。而RHS
所查詢的目的是爲了引用(相似於變量會位於賦值符號=
的右邊),例如第一步查找變量foo
的過程。 面試
咱們知道代碼不只僅能夠訪問當前的做用域的變量,對於嵌套的父級做用域中的變量也能夠訪問。咱們先只在ES5中表述,咱們知道JavaScript在ES5中是沒有塊級做用域的,只有函數能夠建立做用域。舉個例子: 閉包
function Outer(){
var outer = 'outer';
Inner();
function Inner(){
var inner = 'inner';
console.log(outer,inner) // outer inner
}
}複製代碼
當引擎執行到函數Inner
內部的時候,不只能夠訪問當前做用域並且能夠訪問到Outer
的做用域,從而能夠訪問到標識符outer
。所以咱們發現當多個做用域相互嵌套的時候,就造成了做用域鏈。詞法做用域在查找標識符的時候,優先在本做用域中查找。若是在本做用域沒有找到標識符,會繼續向上一級查找,當抵達最外層的全局做用域仍然沒有找到,則會中止對標識符的搜索。若是沒有查找到標識符,會根據不一樣的查找方式做出不一樣的反應。若是是RHS
,則會拋出Uncaught ReferenceError
的錯誤,若是是LHS
,則會在查找最外層的做用域聲明該變量,這就解釋了爲何對未聲明的變量賦值後該變量會成爲全局變量。因此上面的代碼執行ide
console.log(outer,inner)
函數
的時候,引擎會首先要求Inner
函數的詞法做用域查找(RHS
)標識符outer
,被告知該詞法做用域不存在該標識符,而後引擎會要求嵌套的上一級Outer
詞法做用域查找(RHS
)標識符outer
,Outer
詞法做用域的查找成功並將結果返回給引擎。ui
上面咱們理解做用域鏈都是從做用域鏈查找變量的角度去考慮的,其實這已經足夠了,大部分做用域鏈的場景都是查找標識符。可是咱們能夠換一個角度去理解做用域鏈。其實JavaScript的每一個函數都有對應的執行環境(execution context)。當執行流進入進入一個函數時,該函數的執行環境就會被推入環境棧,當函數執行結束以後,該函數的執行環境就會被彈出環境棧,執行環境被變動爲以前的執行環境。而每建立一個執行環境時,會同時生成一個變量對象(variable object)(函數生成的是活動變量(activation object)),用來存儲當前執行環境中定義的變量和函數,當執行環境結束時,當前的變量(活動)對象就會被銷燬(全局的變量對象是一直存在的,不會被銷燬)。雖然咱們沒法訪問到變量(活動)對象,但詞法做用域查找標識符會使用它。
當對於函數的執行環境生成的活動對象,初始化就會存在兩個變量:this
和arguments
,所以咱們在函數中就直接可使用這兩個變量。對於做用域鏈存儲都是變量(活動)對象,而當前執行環境的變量對象就存儲在做用域鏈的最前端,優先被查找。從這個角度看,標識符解析是沿着做用域鏈一級一級地在變量(活動)對象中搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直至找到標識符爲止。
這年頭出去面試JavaScript的崗位,各個都要問你閉包的問題,開始的時候以爲閉包的概念蠻高級的,後來以爲這個也沒啥東西可講的。老早的以前就寫過一篇關於閉包的文章淺談JavaScript閉包,講到如今我以爲把閉包放到做用域鏈一塊兒將會更好。仍是繼續講個例子:
function fn(){
var a = 'JavaScript';
function func(){
console.log(a);
}
return func;
}
var func = fn();
func(); //JavaScript複製代碼
首先明確一下什麼是閉包?我認爲閉包最好的概念解釋就是:
函數在定義的詞法做用域之外的地方被調用,閉包使得函數能夠繼續訪問定義時的詞法做用域。
func
函數執行的位置和定義的位置是不相同的,func
是在函數fn
中定義的,但執行倒是在全局環境中,雖然是在全局函數中執行的,但函數仍然能夠訪問當定義時的詞法做用域。以下圖所示:
咱們以前說過,當函數執行結束後其活動變量就會被銷燬,可是在上面的例子中卻不是這個樣子。但函數fn
執行結束以後,fn
對象的活動變量並無被銷燬,這是由於fn
返回的函數func
的做用域鏈還保持着fn
的活動變量,所以JavaScript的垃圾回收機制不會回收fn
活動變量。雖然返回的函數func
是在全局環境下執行的,可是其做用域鏈的存儲的活動(變量)對象的順序分別是:func
的活動變量、fn
的活動變量、全局變量對象。所以在func
函數執行時,會順着做用域鏈查找標識符,也就能訪問到fn
所定義的詞法做用域(即fn
函數的活動變量)也就不足爲奇了。這樣看起來是否是以爲閉包也是很是的簡單。
說完了做用域鏈,咱們來說講原型鏈。首先也是要明確什麼是原型?全部的函數都有一個特殊的屬性: prototype
(原型),prototype
屬性是一個指針,指向的是一個對象(原型對象),原型對象中的方法和屬性均可以被函數的實例所共享。所謂的函數實例是指以函數做爲構造函數建立的對象,這些對象實例均可以共享構造函數的原型的方法。舉個例子:
var Person = function(name){
this.name = name;
}
Person.prototype.sayName = function(){
console.log('name: ', this.name)
};
var person = new Person('JavaScript');
person.sayName(); //JavaScript複製代碼
在上面的例子中,對象person
是構造函數Person
建立的實例。所謂的構造函數也只不過是普通的函數經過操做符new
來調用。在使用new
操做符調用函數時主要執行如下幾個步驟:
經過構造函數返回的對象,其中含有一個內部指針[[Prototype]]
指向構造函數的原型對象,固然咱們是沒法訪問到這個標準的內部指針[[Prototype]]
,可是在Firefox、Safari和Chrome在上都支持一個屬性__proto__
,用來指向構造函數的原型對象。下圖就解釋了上面的結構:
咱們能夠看到,構造函數Person
的prototype
屬性指向Prototype
的原型對象。而person
做爲構造函數Person
建立的實例,其中存在內部指針也指向Person
的原型對象。須要注意的是,在Person
的原型對象中存在一個特殊的屬性constructor
,指向構造函數Person
。在咱們的例子中,執行到:
person.sayName(); //JavaScript複製代碼
當執行person
的sayName
屬性時,首先會在對象實例中查找sayName
屬性,當發現對象實例中不存在sayName
時,會轉而去搜索person
內部指針[[Prototpe]]
所指向的原型對象,當發現原型對象中存在sayName
屬性時,執行該屬性。關於函數sayName
中this
的指向,有興趣能夠戳這篇文章一個小小的JavaScript題目。
講完了原型,再講講原型鏈,其實咱們上面的圖並不完整,由於全部函數的默認原型都是Object的實例,因此函數原型實例的內部指針[[Prototype]]
指向的是Object.prototype
,讓咱們繼續來完善一下:
person.toString()複製代碼
執行上面代碼時,首先會在對象實例person
中查找屬性toString
方法,咱們發現實例中不存在toString
屬性。而後咱們轉到person
內部指針[[Prototype]]
指向的Person
原型對象去尋找toString
屬性,結果是仍然不存在。這找不到咱們就放棄了?開玩笑,咱們這麼有毅力。咱們會再接着到Person
原型對象的內部指針[[Prototype]]
指向的Object
原型對象中查找,此次咱們發現其中確實存在toString
屬性,而後咱們執行toString
方法。發現了沒有,這一連串的原型造成了一條鏈,這就是原型鏈。
其實咱們上面例子中對屬性toString
查找屬於RHS
,以RHS
方式尋找屬性時,會在原型鏈中依次查找,若是在當前的原型中已經查找到所須要的屬性,那麼就會中止搜索,不然會一直向後查找原型鏈,直到原型鏈的結尾(這一點有點相似於做用域鏈),若是直到原型鏈結尾仍未找到,那麼該屬性就是undefined
。但執行LHS
方式的查找卻大相徑庭,當發現對象實例自己不存在該屬性,直接在該對象實例中聲明變量,而不會去查找原型鏈。例如:
person.toString = function(){
console.log('person')
}
person.toString(); //person複製代碼
當對person
執行LHS
的方式查找toString
屬性時,咱們發現person
中並不存在toString
,這時會直接在person
中聲明屬性,而不會去查找原型鏈,接着咱們執行person.toString()
時,咱們在實例中找到了toString
屬性並將其執行,這樣實例中的toString
就屏蔽了原型鏈中的toString
屬性。
講完了做用域鏈和原型鏈,咱們能夠比較一下。做用域鏈的做用主要用於查找標識符,看成用域須要查詢變量的時候會沿着做用域鏈依次查找,若是找到標識符就會中止搜索,不然將會沿着做用域鏈依次向後查找,直到做用域鏈的結尾。而原型鏈是用於查找引用類型的屬性,查找屬性會沿着原型鏈依次進行,若是找到該屬性會中止搜索並作相應的操做,不然將會沿着原型鏈依次查找直到結尾。
若是以爲閱讀完了本篇文章對你有些許幫助,歡迎你們我關注個人掘金帳號或者star個人Github的blog項目,也算是對個人鼓勵啦!