前面的話
javascript擁有一套設計良好的規則來存儲變量,并且之后可以方便地找到這些變量,這套規則被稱為作用域。作用域貌似簡單,實則復雜,由于作用域與this機制非常容易混淆,使得理解作用域的原理更為重要。本文是深入理解javascript作用域系列的第一篇——內部原理
內部原理分成編譯、執行、查詢、嵌套和異常五個部分進行介紹,最后以一個實例過程對原理進行完整說明
編譯
以var a = 2;為例,說明javascript的內部編譯過程,主要包括以下三步:
【1】分詞(tokenizing)
把由字符組成的字符串分解成有意義的代碼塊,這些代碼塊被稱為詞法單元(token)
var a = 2;被分解成為下面這些詞法單元:var、a、=、2、;。這些詞法單元組成了一個詞法單元流數組
// 詞法分析后的結果 [ "var" : "keyword", "a" : "identifier", "=" : "assignment", "2" : "integer", ";" : "eos" (end of statement) ]
【2】解析(parsing)
把詞法單元流數組轉換成一個由元素逐級嵌套所組成的代表程序語法結構的樹,這個樹被稱為“抽象語法樹” (Abstract Syntax Tree, AST)
var a = 2;的抽象語法樹中有一個叫VariableDeclaration的頂級節點,接下來是一個叫Identifier(它的值是a)的子節點,以及一個叫AssignmentExpression的子節點,且該節點有一個叫Numericliteral(它的值是2)的子節點
{ 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】編譯
1、編譯器查找作用域是否已經有一個名稱為a的變量存在于同一個作用域的集合中。如果是,編譯器會忽略該聲明,繼續進行編譯;否則它會要求作用域在當前作用域的集合中聲明一個新的變量,并命名為a
2、編譯器將var a = 2;這個代碼片段編譯成用于執行的機器指令
[注意]依據編譯器的編譯原理,javascript中的重復聲明是合法的
//test在作用域中首次出現,所以聲明新變量,并將20賦值給test var test = 20; //test在作用域中已經存在,直接使用,將20的賦值替換成30 var test = 30;
【2】執行
1、引擎運行時會首先查詢作用域,在當前的作用域集合中是否存在一個叫作a的變量。如果是,引擎就會使用這個變量;如果否,引擎會繼續查找該變量
2、如果引擎最終找到了變量a,就會將2賦值給它。否則引擎會拋出一個異常
查詢
在引擎執行的第一步操作中,對變量a進行了查詢,這種查詢叫做LHS查詢。實際上,引擎查詢共分為兩種:LHS查詢和RHS查詢
從字面意思去理解,當變量出現在賦值操作的左側時進行LHS查詢,出現在右側時進行RHS查詢
更準確地講,RHS查詢與簡單地查找某個變量的值沒什么區別,而LHS查詢則是試圖找到變量的容器本身,從而可以對其賦值
function foo(a){ console.log(a);//2 } foo( 2 );
這段代碼中,總共包括4個查詢,分別是:
1、foo(...)對foo進行了RHS引用
2、函數傳參a = 2對a進行了LHS引用
3、console.log(...)對console對象進行了RHS引用,并檢查其是否有一個log的方法
4、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
文章列表