又是一年臨近年底了,年底制定下了許多計劃,正在一點一點實現,最近在開始讀《你不知道的Javascript》了,也會慢慢把讀書筆記通過部落格的形式輸出出來,讓自己印象更深刻,今天就來聊聊JS中的var a = 2;
這行程式碼發生了什麼?
編譯
對於程式語言來說都會有一個編譯的過程,一段程式碼在執行前大多都會經歷下面幾個步驟:(具體的細節會根據語言特性而異)
- 分詞/詞法分析(Tokenizing/Lexing)
這個過程會將由字元組成的字串分解成(對程式語言來說)有意義的程式碼塊,這些代
碼塊被稱為詞法單元(token)。例如,考慮程式 var a = 2;。這段程式通常會被分解成
為下面這些詞法單元:var、a、=、2 、;。
- 解析/語法分析(Parsing)
這個過程是將詞法單元流(陣列)轉換成一個由元素逐級巢狀所組成的代表了程式語法
結構的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)。
- 程式碼生成
將 AST 轉換為可執行程式碼的過程稱被稱為程式碼生成。這個過程與語言、目標平臺等息
息相關。
拋開具體細節,簡單來說就是有某種方法可以將 var a = 2; 的 AST 轉化為一組機器指
令,用來建立一個叫作 a 的變數(包括分配記憶體等),並將一個值儲存在 a 中。
角色
- 引擎
從頭到尾負責整個 JavaScript 程式的編譯及執行過程。
- 編譯器
負責語法分析及程式碼生成等
- 作用域
負責收集並維護由所有宣告的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權。
執行
我們在瞭解了這些編譯中的基本概念後,我們在來仔細看看var a = 2;
這行程式碼發生了什麼;
- 首先編譯器遇到這句話時會先將這段程式分解成詞法單元,然後把詞法單元解析成一個樹結構也就是AST,然後進行程式碼生成
- 遇到
var a
,編譯器會詢問作用域是否已經有一個該名稱的變數存在於同一個作用域的集合中。如果是,編譯器會忽略該宣告,繼續進行編譯;否則它會要求作用域在當前作用域的集合中宣告一個新的變數,並命名為 a。(變數宣告階段) - 接下來編譯器會為引擎生成執行時所需的程式碼,這些程式碼被用來處理 a = 2 這個賦值操作。引擎執行時會首先詢問作用域,在當前的作用域集合中是否存在一個叫作 a 的變數。如果是,引擎就會使用這個變數;如果否,引擎會繼續查詢該變數。如果引擎最終找到了 a 變數,就會將 2賦值給它。否則引擎就會舉手示意並丟擲一個異常 (變數賦值階段)
總結:變數的賦值操作會執行兩個動作,首先編譯器會在當前作用域中宣告一個變數(如果之前沒有宣告過),然後在執行時引擎會在作用域中查詢該變數,如果能夠找到就會對它賦值。
編譯器查詢變數
我們知道了var a = 2;js的執行過程了,那麼如果是var a = b;呢? 那麼這個過程對於b來說我們如何查到變數b的值呢;
編譯器查詢變數有兩種型別,一直是LHS,一種是RHS,舉個例子:
a = 2
複製程式碼
這個例子就是LHS查詢,當編譯器去查詢a變數的時候,其實是為了找到a變數的容器,去給a賦值,所以LHS查詢其實是查詢變數的容器
console.log(a)
複製程式碼
這個例子中的其實就是RHS查詢,當編譯器去查詢a變數的時候,其實目的是找到a變數的值而不是他的容器,因此RHS查詢其實是查詢變數的值
ok,我們知道了之後我們來嘗試一下一個進階例子。
function foo(a) { // 隱式的進行引數賦值 將a = 2; 進行LHS查詢 查詢到a容器 並進行賦值
var b = a; // RHS進行查詢 查詢到a變數的值 然後 LHS查詢 查詢到b容器並進行賦值
return a + b;// RHS進行查詢 查詢到a和b變數的值
}
var c = foo( 2 ); // 編譯器先RHS查詢foo變數 然後進入到foo函式體 函式執行完成後,RHS查詢查詢變數c的容器並進行賦值
複製程式碼
異常
- 當進行RHS變數查詢的時候,如果沿著作用域鏈一直查詢 並沒有找到變數的時候,這個時候會丟擲ReferrenceError
- 當進行LHS變數查詢的時候,如果沿著作用域鏈一直查詢 並沒有找到變數的時候,如果是在非嚴格模式下 那麼就會在全域性作用域中建立一個這樣的變數並且進行賦值操作,如果是在嚴格模式下也會丟擲ReferrenceError的錯誤
注:如果RHS查到了一個變數,但是這個變數的型別和我們將要進行的操作不同時,比如試圖對一個非函式型別的值進行函式呼叫,那麼就會丟擲TypeError的錯誤