一、什么是作用域
存儲和訪問變量,是任何一種編程語言最基本的功能之一,變量存在哪里?程序需要時如何找到它?這些問題需要一套良好的規則來規范,這套規則,就成為作用域。
二、編譯原理
js通常歸類為解釋語言,但它其實是編譯語言,和傳統編譯語言不同,它不是提前編譯,編譯結果也不能在分布式系統中進行移植。js引擎進行編譯的步驟和傳統的編譯語言非常相似,傳統編譯語言,程序中的一段源碼在執行之前會經歷三個步驟,統稱為“編譯”。
2.1 分詞/詞法分析
這個過程會講由字符組成的祖父穿分解成有意義的代碼塊。這些代碼塊叫詞法單元。例如:var a=2;會分解成以下這些詞法單元:
var、a、=、2、;
空格是否會被當做詞法單元,取決於空格在這門語言中是否具有意義。
2.2 解析/詞法分析
這個過程是將詞法單元流轉成一個由元素逐級嵌套所組成的代表了程序語法結構的樹,這個樹叫做抽象語法樹(AST)。
var a=2; 的抽象語法數中可能會有一個叫做VariableDeclaration的頂級節點,接下來是一個叫做Identifier(它的值是a)的節點,以及一個叫做AssignmentExpression的子節點,AssignmentExpression節點有一個叫做NumericLiteral(它的值是2)的子節點。
2.3 代碼生成
將AST轉換成可執行代碼的過程叫做代碼生成,簡單來說就是有某種方法可以講var a=2 ;的AST轉化成一組機器指令,用來創建一個叫做a的變量(包括分配內存等),並將一個值存在a中。
比起那些編譯過程只要三個步驟的語言編譯器,js引擎要復雜得多,例如,在語法分析和代碼生成階段有特定的步驟來對運行性能進行優化,包括對冗余元素進行優化等。簡單來說,任何js代碼片段在執行前都要進行編譯(通常就在執行前),因此js編譯器首先會對var a=2;這段程序進行編譯,然后做好執行他的准備,並且通常馬上就會執行它。
三、理解作用域
3.1 參與到var a=2;進行處理的過程中的演員們
引擎:從頭到尾負責整個js程序的編譯和執行過程
編譯器:引擎的好朋友,負責詞法分析以及代碼生成等臟活累活。
作用域:引擎的另外一個朋友,負責收集並維護由所有聲明的標識符(變量)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限。
3.2 對話
看看var a=2;的執行過程。
編譯器首先會將這段代碼分解成詞法單元,然后將詞法單元解析成一個樹結構,但是當編譯器進行代碼生成時,他對這段程序的處理方式和預期的有所不同。可以合理地假設編譯器所產生的代碼能夠用下面的偽代碼進行概括:“為一個變量分配內存,將其命名為a,然后將值2保存在這個變量”,然而,這並不完全正確。
事實上編譯器會進行如下處理:
遇到var a,編譯器會詢問作用域是否已經有一個該名稱的變量存在於同一個作用域的集合中,如果是,編譯器會忽略該聲明,繼續進行編譯,否則他會要求作用域在當前作用域的集合中聲明一個新的變量,命名為a。
接下來編譯器會為引擎聲稱運行時所需的代碼,這些代碼備用處理a=2這個賦值操作。引擎運行時會首先詢問作用域,在當前的作用域集合中是否存在一個叫做a的變量,如果是,引擎會使用這個變量,否則,引擎會繼續查找變量。
如果引擎最終找到了a變量,就會將2賦值給它,否則引擎會舉手示意拋出一個異常。
所以,變量的復制操作會執行兩個動作,首先編譯器會在當前作用域中聲明一個變量(如果之前沒有聲明過),然后再運行時引擎會在作用域中查找這個變量,如果能夠找到就會對他賦值。
3.3 編譯器有話說
編譯器在編譯過程的第二步中生成了代碼,引擎執行它時,會通過查找變量a來判斷是否已聲明過,查詢的過程由作用域協助,但是引擎執行怎么樣的查詢,會影響最終的查詢結果。
引擎為會a進行LHS查詢,另外一個查詢的類型叫做RHS。
當變量出現在賦值操作的左側時,進行LHS查詢,出現在右側時進行RHS查詢。
console.log(a); //對a的引用是一個RHS引用
a=2;//對a的引用是LHS引用
三、作用域嵌套
當一個塊或者函數嵌套在另外一個塊或者函數中時,就發生了作用域嵌套,在當前作用域中無法找到這個變量時,引擎就會在外層嵌套的作用域中繼續找,直到找到該變量,或抵擋最外層的作用域位置。
function foo(a){ console.log(a+b); } var b=2; foo(2);
引擎:foo作用域兄弟,你見過b嗎?我需要對它進行RHS引用。
作用域:沒有
引擎:foo的上級作用域兄弟,你見過b沒?我需要對它進行RHS引用。
作用域:當然了,給你!
四、總結
作用域是一套規則,用於確定在何處以及如何查找變量,如果查找的目的是對變量賦值,那么就使用LHS查詢,如果目的是獲取變量的值,就用RHS查詢,