javascript擁有一套設計良好的規則來存儲變量,而且以後能夠方便地找到這些變量,這套規則被稱爲做用域。做用域貌似簡單,實則複雜,因爲做用域與this機制很是容易混淆,使得理解做用域的原理更爲重要。本文是深刻理解javascript做用域系列的第一篇——內部原理javascript
內部原理分紅編譯、執行、查詢、嵌套和異常五個部分進行介紹,最後以一個實例過程對原理進行完整說明html
以var a = 2;爲例,說明javascript的內部編譯過程,主要包括如下三步:java
【1】分詞(tokenizing)數組
把由字符組成的字符串分解成有意義的代碼塊,這些代碼塊被稱爲詞法單元(token)ide
var a = 2;被分解成爲下面這些詞法單元:var、a、=、二、;。這些詞法單元組成了一個詞法單元流數組函數
// 詞法分析後的結果 [ "var" : "keyword", "a" : "identifier", "=" : "assignment", "2" : "integer", ";" : "eos" (end of statement) ]
【2】解析(parsing)優化
把詞法單元流數組轉換成一個由元素逐級嵌套所組成的表明程序語法結構的樹,這個樹被稱爲「抽象語法樹」 (Abstract Syntax Tree, AST)this
var a = 2;的抽象語法樹中有一個叫VariableDeclaration的頂級節點,接下來是一個叫Identifier(它的值是a)的子節點,以及一個叫AssignmentExpression的子節點,且該節點有一個叫Numericliteral(它的值是2)的子節點spa
{ operation: "=", left: { keyword: "var", right: "a" } right: "2" }
【3】代碼生成設計
將AST轉換爲可執行代碼的過程被稱爲代碼生成
var a=2;的抽象語法樹轉爲一組機器指令,用來建立一個叫做a的變量(包括分配內存等),並將值2儲存在a中
實際上,javascript引擎的編譯過程要複雜得多,包括大量優化操做,上面的三個步驟是編譯過程的基本概述
任何代碼片斷在執行前都要進行編譯,大部分狀況下編譯發生在代碼執行前的幾微秒。javascript編譯器首先會對var a=2;這段程序進行編譯,而後作好執行它的準備,而且一般立刻就會執行它
簡而言之,編譯過程就是編譯器把程序分解成詞法單元(token),而後把詞法單元解析成語法樹(AST),再把語法樹變成機器指令等待執行的過程
實際上,代碼進行編譯,還要執行。下面仍然以var a = 2;爲例,深刻說明編譯和執行過程
【1】編譯
一、編譯器查找做用域是否已經有一個名稱爲a的變量存在於同一個做用域的集合中。若是是,編譯器會忽略該聲明,繼續進行編譯;不然它會要求做用域在當前做用域的集合中聲明一個新的變量,並命名爲a
二、編譯器將var a = 2;這個代碼片斷編譯成用於執行的機器指令
[注意]依據編譯器的編譯原理,javascript中的重複聲明是合法的
//test在做用域中首次出現,因此聲明新變量,並將20賦值給test var test = 20; //test在做用域中已經存在,直接使用,將20的賦值替換成30 var test = 30;
【2】執行
一、引擎運行時會首先查詢做用域,在當前的做用域集合中是否存在一個叫做a的變量。若是是,引擎就會使用這個變量;若是否,引擎會繼續查找該變量
二、若是引擎最終找到了變量a,就會將2賦值給它。不然引擎會拋出一個異常
在引擎執行的第一步操做中,對變量a進行了查詢,這種查詢叫作LHS查詢。實際上,引擎查詢共分爲兩種:LHS查詢和RHS查詢
從字面意思去理解,當變量出如今賦值操做的左側時進行LHS查詢,出如今右側時進行RHS查詢
更準確地講,RHS查詢與簡單地查找某個變量的值沒什麼區別,而LHS查詢則是試圖找到變量的容器自己,從而能夠對其賦值
function foo(a){ console.log(a);//2 } foo( 2 );
這段代碼中,總共包括4個查詢,分別是:
一、foo(...)對foo進行了RHS引用
二、函數傳參a = 2對a進行了LHS引用
三、console.log(...)對console對象進行了RHS引用,並檢查其是否有一個log的方法
四、console.log(a)對a進行了RHS引用,並把獲得的值傳給了console.log(...)
在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量,或抵達最外層的做用域(也就是全局做用域)爲止
function foo(a){ console.log( a + b ) ; } var b = 2; foo(2);// 4
在代碼片斷中,做用域foo()函數嵌套在全局做用域中。引擎首先在foo()函數的做用域中查找變量b,並嘗試對其進行RHS引用,沒有找到;接着,引擎在全局做用域中查找b,成功找到後,對其進行RHS引用,將2賦值給b
爲何區分LHS和RHS是一件重要的事情?由於在變量尚未聲明(在任何做用域中都沒法找到變量)的狀況下,這兩種查詢的行爲不同
RHS
【1】若是RHS查詢失敗,引擎會拋出ReferenceError(引用錯誤)異常
//對b進行RHS查詢時,沒法找到該變量。也就是說,這是一個「未聲明」的變量 function foo(a){ a = b; } foo();//ReferenceError: b is not defined
【2】若是RHS查詢找到了一個變量,但嘗試對變量的值進行不合理操做,好比對一個非函數類型值進行函數調用,或者引用null或undefined中的屬性,引擎會拋出另一種類型異常:TypeError(類型錯誤)異常
function foo(){ var b = 0; b(); } foo();//TypeError: b is not a function
LHS
【1】當引擎執行LHS查詢時,若是沒法找到變量,全局做用域會建立一個具備該名稱的變量,並將其返還給引擎
function foo(){ a = 1; } foo(); console.log(a);//1
【2】若是在嚴格模式中LHS查詢失敗時,並不會建立並返回一個全局變量,引擎會拋出同RHS查詢失敗時相似的ReferenceError異常
function foo(){ 'use strict'; a = 1; } foo(); console.log(a);//ReferenceError: a is not defined
function foo(a){ console.log(a); } foo(2);
以上面這個代碼片斷來講明做用域的內部原理,分爲如下幾步:
【1】引擎須要爲foo(...)函數進行RHS引用,在全局做用域中查找foo。成功找到並執行
【2】引擎須要進行foo函數的傳參a=2,爲a進行LHS引用,在foo函數做用域中查找a。成功找到,並把2賦值給a
【3】引擎須要執行console.log(...),爲console對象進行RHS引用,在foo函數做用域中查找console對象。因爲console是個內置對象,被成功找到
【4】引擎在console對象中查找log(...)方法,成功找到
【5】引擎須要執行console.log(a),對a進行RHS引用,在foo函數做用域中查找a,成功找到並執行
【6】因而,引擎把a的值,也就是2傳到console.log(...)中
【7】最終,控制檯輸出2