做用域,這個詞在編程界常常能聽到看到,每個程序員幾乎都有被問到過。在前端圈,面試JavaScript相關知識,這能夠算說是一個很是基礎的問題了。但早年間我長期陷入了一種「只可意會不可言傳」的地步,我不知道是否是有許多小夥伴與我曾經有同樣的經歷,因此我就抽時間把書本中看到的東西整理了一下。把提煉的東西分享給你們,若有不正確之處煩請指正。可能大多對做用域的通用解釋是這種:前端
做用域就是變量(標識符)適用範圍,控制着變量的可見性。程序員
但他具體是什麼,是一個區域?仍是一種規則呢?面試
我記得《JavaScript權威指南》中對變量做用域有這麼一段描述:編程
一個變量的做用域(scope)是程序源代碼中定義這個變量的區域。全局變量擁有全局做用域,在JavaScript代碼中的任何地方都是有定義的。然而在函數內聲明的變量只在函數體內有定義。它們是局部變量,做用域是局部性的。函數參數也是局部變量,它們只是在函數體內有定義。數組
這段描述大體的告訴了讀者做用域是個啥,能夠在這裏理解爲一個「區域」。bash
兩年前我第一次看到這句話仍是答不出做用域是啥,雖然已經在腦海裏有一個大體的輪廓。我以爲我應該繼續深究一下,做用域到底是啥。框架
要搞清楚做用域是啥,咱們須要或多或少的知道一點點JavaScript的編譯原理,從第一次接觸JavaScript開始,我接觸到的全部知識就告訴我,JavaScript是一門「動態」或「解釋執行」語言,但後來我才知道,它其實是一門編譯語言。是否是很驚訝?與傳統的編譯語言不一樣的是,JavaScript不是提早編譯的,編譯結果也不能在分佈式系統中進行移植。編程語言
那麼編譯過程是啥呢?能夠分爲這麼三步:分佈式
var a = 2;
。這段程序一般會被分解成下面這些詞法單元:var、a、=、二、;。空格是否會被看成詞法單元,取決於空格在這門語言中是否具備意義。var a = 2;
的抽象語法樹中可能會有一個叫作VariableDeclaration的頂級節點,接下來是一個叫作Identifier(它的值是a)的子節點,以及一個叫作AssignmentExpression的子節點。AssignmentExpression節點有一個叫作NumericLiteral(它的值是2)的子節點。var a = 2;
的AST轉化爲一組機器指令,用來建立一個叫作a的變量(包括分配內存等),並將一個值存儲在a中。固然,對比就這三步的編譯語言來講,JavaScript引擎要複雜的多。但JavaScript的編譯大多發生在代碼執行前的幾微秒,甚至更短。在咱們要討論的做用域背後,JavaScript引擎用盡了各類方法(好比JIT,能夠延遲編譯甚至實施重編譯)來保證最佳性能。函數
在這裏,咱們會無數次用到做用域這個詞,你徹底能夠按照以前的理解來閱讀。這並不影響咱們最終對做用域的理解。
仍是var a = 2
這行代碼,經過上面的什麼是編譯部分咱們能夠知道,編譯器首先會將這段代碼分解成詞法單元,而後將詞法單元解構成一個樹結構(AST),可是當編譯器開始進行代碼生成時,它對這段代碼的處理方式會和預期的有所不一樣。
當咱們看到這行代碼,用僞代碼進行跟別人進行歸納時,可能會這樣去表述:「爲一個變量分配內存,並將其命名爲a,而後將值2保存到這個變量(內存)中。」 然而,這並不徹底正確。
事實上編譯器會進行以下操做:
var a
,編譯器會詢問做用域是否已經有一個該名稱的變量存在於同一個做用域的集合中。若是是,編譯器會忽略該聲明,繼續進行編譯;不然它會要求做用域在當前做用域的集合中聲明一個新的變量,並命名爲a。a = 2
這個賦值操做。引擎運行時會首先詢問做用域,在當前的做用域集合中,是否存在一個叫做a的變量,若是是,引擎就會使用這個變量;若是否,引擎就會繼續查找該變量。總結起來就是:一、編譯器在做用域聲明變量(若是沒有);二、引擎在運行這些代碼時查找該變量,若是有就進行賦值;
在上面的第二步中,引擎執行「運行時所需的代碼」時,會經過查找變量a來判斷它是否已經聲明過。查找的過程由做用域進行協助,但時引擎執行怎麼查找,會影響最終的查找結果。
仍是var a = 2;
這個例子,引擎會爲變量a進行LHS查詢。固然還有一種RHS查詢。那麼LHS和RHS查詢是什麼呢?這裏的L表明左側,R表明右側。通俗且不嚴謹的解釋LHS和RHS的含義就是:當變量出如今賦值操做的左側時進行LHS查詢,出如今右側時進行RHS查詢。
那麼描述的更準確的一點,RHS查詢與簡單的查找某個變量的值毫無二致,而LHS查詢則是試圖找到變量的容器自己,從而能夠對其賦值。從這個角度說,RHS並非真正意義上的「賦值操做的右側」,更準確的說是「非左側」。因此,咱們能夠將RHS理解成Retrieve his source value(取到它的源值),這意味着,「獲得某某的值」。
那咱們來看一段代碼深刻理解一下LHS與RHS。
function foo(a) {
console.log(a)
}
foo(2)
複製代碼
從這段代碼中,咱們先看看: console.log(a)
其中a的引用是一個RHS引用,由於咱們是取到a的值。並將這個值傳遞給console.log(...)方法。
相比之下,例如: a = 2 // 調用foo(2)時,隱式的進行了賦值操做
這裏對a的引用就是LHS引用,由於咱們實際上不關心當前的值時什麼,只要想把=2這個賦值操做找到一個目標。
固然上面的程序並不僅有一個LHS和RHS引用:
function foo(a) {
// 這裏隱式的進行了對形參a的LHS引用。
// 這裏對log()方法進行了RHS引用,詢問console對象上是否有log()方法。
// 對log(a)方法內的a進行RHS引用,取到a的值。
console.log(a)
}
// 此處調用foo()方法,須要調用對foo的RHS引用。意味着「去找foo這個值,並把它給我。」
foo(2)
複製代碼
須要注意的是:咱們常常會將函數聲明function foo(a) {...} 轉化爲普通的變量賦值 var foo = function(a) {...},這樣去理解的話,這個函數是LHS查詢。可是有一個細微的差異,編譯器能夠在代碼生成的同時處理聲明和值的定義,好比引擎執行代碼時,並不會有線程專門用來將一個函數值「分配給」foo,所以,將函數聲明理解成前面討論的LHS查詢和賦值的形式並不合適。
到這裏,是否對做用域的工做有了一個理解呢?可是它是什麼,仍是有些模糊,不知道該怎麼去表述。先無論,先看看什麼是做用域鏈。
問道做用域,跑不掉的就是做用域鏈了,咱們來看一個代碼例子:
function foo(a) {
console.log(a + b)
}
var b = 2;
foo(2); // 4
複製代碼
經過上面咱們得知,對b的RHS引用沒法在函數內部完成的,由於函數內部並無定義b,可是在這個例子中,咱們能夠在上一級做用域(這裏是全局做用域)中完成。
那麼這個查找規則就很簡單了:引擎從當前的執行做用域開始查找變量,若是找不到,就像上一級繼續查找,當抵達最外層的全局做用域時,不管找沒找到,查找都會中止。那麼這麼一個自上而下的查找關係,是一個鏈式的查找關係。
那麼沒找到會發生什麼呢?進行RHS引用時,若是RHS查詢全部的嵌套的做用域中遍尋不到所需的變量,引擎就會拋出一個ReferenceError的異常。
相較之下,當引擎執行LHS查詢時,若是全局做用域下都沒法找到目標變量,全局做用域中就會建立一個具備該名稱的變量,並返回給引擎。前提是該程序運行在非「嚴格模式」下。反之則會拋出ReferenceError異常。
那麼在寫代碼過程當中,ReferenceError異常與做用域判別失敗相關,而TypeError則表明着做用域判別成功,可是對結果的操做時不合法的。
因此,寫到這裏,對做用域是幹什麼的有了一個比較清晰的理解呢? 好,我來試着從新表述一下什麼是做用域:
做用域是一套「標識符的查詢規則」(注意我這裏用的詞是規則),根據查找的目的進行LHS與RHS查詢。肯定了在何處(當前做用域、上級做用域...全局做用域)如何查找(LHS、RHS)。
固然,這篇文章也發佈在個人我的博客《再談JavaScript做用域》,有興趣的小夥伴能夠看一看。
Flanagan. JavaScript權威指南[M]. 北京:機械工業出版社, 2012. Kyle Simpson. 你不知道的JavaScript[M]. 北京:人民郵電出版社, 2015.