問題:變量存儲在哪裏?更重要的是,在需要的時候程序如何找到他們?
1.1 編譯原理:
儘管通常將JavaScript歸類爲“動態”或者“解釋執行”語言,但事實上它是一門編譯語言。
但與傳統的編譯語言不同,他不是提前編譯的,編譯結果也不能在分佈式系統中進行移植。 ----《你不知道的JavaScript》
傳統編譯語言的編譯過程
(以 var a = 2 爲例):
1、分詞/詞法分析(Tokenizing/Lexing):
將 var a = 2 分解成var、a、=、2、; 、等詞法單元(token)(空格是否被當做單元格取決於語言)。
2、解析/語法分析(Parsing):
將詞法單元流(數組)轉換爲一個“抽象語法樹” (AST);
頂節點 VariableDeclaration(個人理解爲var,變量聲明);
一個 Identifier 子節點(值爲a);
一個 NumericLiteral 子節點(值爲2,個人理解爲number類型)。
3、代碼生成
將第二部生成的AST轉換爲代碼,簡單的說就是講AST轉化爲一組機械指令,創建叫做a的變量(包括分配內存),並將一個值存儲在a中。
JavaScript引擎的編譯
與其他語言不同,JavaScript的編譯過程不是發生在構建之前的。
對於JavaScript來說,大部分情況下編譯發生在代碼執行前的幾微秒(甚至更短)時間內。
1.2 理解作用域
參與程序處理過程的幾個部分:
1、引擎:
從頭到尾負責整個JavaScript程序編譯及執行過程
2、編譯器:
負責分詞、語法分析及代碼生成
3、作用域:
負責收集並維護由變量組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些變量的訪問權限。
(可以記憶成 查找變量的一套規則 )
程序處理具體過程:
(以 var a = 2 爲例):
事實上,引擎會認爲這裏有兩個完全不同的聲明(var a,a = 2),一個由編譯器在編譯時處理,一個由引擎在運行時處理。
編譯器首先會進行編譯(分詞,解析成樹結構,生成代碼)
編譯器生成代碼分爲兩步:
1、var a,編譯器會先詢問作用域,如果當前作用域集合中已存在 a 變量則忽略,否則聲明一個新變量並命名爲 a。
2、a = 2,編譯器爲引擎生成運行時所需代碼,引擎運行時再次也會詢問作用域是否存在 a 變量,若存在則使用,找不到則拋出異常。
總結:變量賦值操作會執行兩個動作,編譯器在作用域中聲明變量(若之前沒聲明過),運行時引擎在作用域中查找變量(找到則賦值)。
爲了進一步理解,介紹一些編譯器的術語
引擎執行編譯器的代碼時會通過查找變量 a 來判斷是否聲明過,查找的過程由作用域協助,但是引擎執行怎樣的查找,會影響最終結果,在我們的例子中,引擎會爲變量 a 進行LHS查詢,另一個查詢的類型叫做RHS。
- LHS查詢 (賦值操作的左側): 查找變量的容器本身,從而賦值。
- RHS查詢 (賦值操作的右側):查找變量的值。
注:LHS和RHS的含義是賦值操作的左右側,然而賦值操作還有其他幾種形式,因此最好在概念上理解爲“賦值操作的目標(LHS)”和“誰是賦值操作的源頭(RHS)”。
還有一個值得注意的地方,函數聲明 function foo(a){... 並不能簡單的理解爲 var foo、foo=function(a){... 。如果這樣理解的話,這個函數聲明需要對 foo 進行LHS查詢,但是並不會有線程專門用來將一個函數值“分配給”foo。因此,函數聲明不適合理解成前面的LHS查詢和賦值的形式。
1.3 作用域嵌套
我們說過,作用域是根據名稱查找變量的一套規則。
當一個塊或函數嵌套在另一個塊或函數中時,就發生了作用域的嵌套。
因此在當前作用域無法找到某個變量時,就會在外層嵌套的作用域繼續找,直到找到該變量,或抵達最外層的作用域(全局作用域)爲止。
1.4 異常
爲什麼區分LHS和RHS是一件重要的事?
因爲在變量還沒有聲明(在任何作用域中都無法找到該變量)的情況下,這兩種查詢的行爲是不一樣的。
RHS查詢 查找不到會拋出 ReferenceError 異常。
LHS查詢 如果在全局作用域中無法找到目標變量時,就會在全局作用域中創建一個具有該名稱的變量,並返回給引擎(非嚴格模式)
ES5中引入了嚴格模式,同正常模式(寬鬆/懶惰模式)相比,嚴格模式禁止自動或者隱式創建全局變量,會拋出同RHS類似的ReferenceError 異常。
如果RHS找到了一個變量,但是你嘗試對該變量進行不合理操作,比如視圖對一個非函數類型的值進行函數調用,或者引用null或undefined類型中的值中的屬性,那麼會拋出 TypeError 異常。
ReferenceError 同作用域判別失敗相關,TypeError 則代表作用域判別成功了,但是對結果的操作是非法或者不合理的。
1.5 小結
作用域是一套規則,用於確定在何處以及如何查找變量(標識符)。如果查找的目的是對變量進行賦值,那麼就會使用LHS查詢;如果目的是獲取變量的值,就會使用RHS查詢。
賦值操作符會導致LHS查詢,=操作符或者調用函數時傳入參數的操作都會導致關聯作用域的賦值操作。
JavaScript引擎首先會在代碼執行前對其編譯,在這個過程中,像var a = 2 這樣的聲明會被分解成兩個獨立的步驟
- 首先,var a 在其作用域中聲明新變量。這會在開始階段,也就是代碼執行前進行。
- 接下來,a = 2 會查詢(LHS查詢)變量 a 並對其賦值
LHS和RHS查詢都會在當前執行作用域中開始,如果有需要,就會向上級作用域繼續查找目標標識符,直到抵達全局作用域(頂層)
不成功的RHS引用會導致拋出ReferenceError異常。不成功的LHS引用會導致自動隱式地創建一個全局變量(非嚴格模式下),該變量使用LHS引用的目標作爲標識符,或者拋出ReferenceError 異常(嚴格模式下)。