什么是Javascript解析引擎?
Javascript解析引擎(簡稱Javascript引擎),是一個程序,是瀏覽器引擎的一個部分。
每個瀏覽器的Javascript解析引擎都不相同(因為每個瀏覽器編寫Javascript解析引擎的語言(C或者C++)以及解析原理都不相同)。標準的Javascript解析引擎會按照 ECMAScript文檔來實現。雖然每個瀏覽器的Javascript解析引擎不同,但Javascript的語言性質決定了Javascript關鍵的渲染原理仍然是動態執行Javascript字符串。只是詞法分析、語法分析、變量賦值、字符串拼接的實現方式有所不同。
JavaScript解析引擎到底是干什么的?
JavaScript解析引擎就是根據ECMAScript定義的語言標準來動態執行JavaScript字符串。雖然之前說現在很多瀏覽器不全是按照標準來的,解釋機制也不盡相同,但動態解析JS的過程還是分成兩個階段:語法檢查階段和運行階段。
語法檢查包括詞法分析和語法分析,運行階段又包括預解析和運行階段(像V8引擎會將JavaScript字符串編譯成二進制代碼,此過程應該歸到語法檢查過程中)。
JavaScript解析過程
在JavaScript解析過程中,如遇錯誤就直接跳出當前代碼塊,直接執行下一個 script 代碼段。所以在同一個 script 內的代碼段有錯誤的話就不會執行下去,但是不會影響下一個 script 內的代碼段。
第一階段:語法檢查
語法檢查也是JavaScript解析器的工作之一,包括 詞法分析 和 語法分析,過程大致如下:
一:詞法分析
詞法分析:JavaScript解釋器先把JavaScript代碼(字符串)的字符流按照ECMAScript標準轉換為記號流。
例如:把字符流:
a = (b - c);
轉換為記號流:
NAME "a"
EQUALS
OPEN_PARENTHESIS
NAME "b"
MINUS
NAME "c"
CLOSE_PARENTHESIS
SEMICOLON
二:語法分析
語法分析:JavaScript語法分析器在經過詞法分析后,將記號流按照ECMAScript標準把詞法分析所產生的記號生成語法樹。
通俗地說就是把從程序中收集的信息存儲到數據結構中,每取一個詞法記號,就送入語法分析器進行分析。
語法分析不做的事:去掉注釋,自動生成文檔,提供錯誤位置(可以通過記錄行號來提供)。ECMAScript標準如下:
- var,if,else,break,continue等是JavaScript的關鍵詞
- abstract,int,long等是JavaScript保留詞
- 怎么樣算是數字、怎么樣算是字符串等等
- 定義了操作符(+,-,=)等操作符
- 定義了JavaScript的語法
- 定義了對表達式,語句等標準的處理算法,比如遇到==該如何處理
- ……
當語法檢查正確無誤之后,就可以進入運行階段了。
第二階段:運行階段
一:預解析
第一步:JavaScript引擎將語法檢查正確后生成的語法樹復制到當前執行上下文中。
第二步:JavaScript引擎會對語法樹當中的變量聲明、函數聲明以及函數的形參進行屬性填充。
“預解析”從語法檢查階段復制過來的信息如下:
- 內部變量表varDecls:varDecls保存的用var進行顯式聲明的局部變量。
- 內嵌函數表funDecls:在“預解析”階段,發現有函數定義的時候,除了記錄函數的聲明外,還會創建一個原型鏈對象(prototype)。
- …其他的信息。
執行上下文(execution context)
(一)預解析階段創建的執行上下文包括:變量對象、作用域鏈、this
- 變量對象(Variable Object):由var declaration、function declaration(變量聲明、函數聲明)、arguments(參數)構成。變量對象是以單例形式存在。
- 作用域鏈(Scope Chain):variable object + all parent scopes(變量對象以及所有父級作用域)構成。
- this值:(thisValue):content object。this值在進入上下文階段就確定了。一旦進入執行代碼階段,this值就不會變了。
(二)“預解析”階段創建執行上下文之后,還會對變量對象/活動對象(VO/AO)的一些屬性填充數值。
- 函數的形參:執行上下文的變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對于沒有傳遞的參數,其值為undefined。
- 函數聲明:執行上下文的變量對象的一個屬性,屬性名和值都是函數對象創建出來的;如果變量對象已經包含了相同名字的屬性,則會替換它的值。
- 變量聲明:執行上下文的變量對象的一個屬性,其屬性名即為變量名,其值為undefined;如果變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的函數聲明的屬性,該聲明會被忽略掉,但其包含的賦值操作不會忽略。
變量對象/活動對象(VO/AO)填充的順序也是按照以上順序:函數的形參->函數聲明->變量聲明;在變量對象/活動對象(VO/AO)中權重高低也按照函數的形參->函數聲明->變量聲明順序來。
如下代碼:
var a=1;
function b(a) {
alert(a);
}
var b;
alert(b); // function b(a) { alert(a); }
b(); //undefined
變量對象/活動對象(VO/AO)填充及優先順序
以上代碼在進入執行上下文時,按照函數的形參->函數聲明->變量聲明順序來填充,并且優先權永遠都是函數的形參>函數聲明>變量聲明,所以只要alert(a)中的a是函數中的形參,就永遠不會被函數和變量聲明覆蓋。就算沒有賦值也是默認填充的undefined值。
第二部分:執行代碼
經過“預解析”創建執行上下文之后,就進入執行代碼階段,VO/AO就會重新賦予真實的值,“預解析”階段賦予的undefined值會被覆蓋。
此階段才是程序真正進入執行階段,Javascript引擎會一行一行的讀取并運行代碼。此時那些變量都會重新賦值。
假如變量是定義在函數內的,而函數從頭到尾都沒被激活(調用)的話,則變量值永遠都是undefined值。
進入了執行代碼階段,在“預解析”階段所創建的任何東西可能都會改變,不僅僅是VO/AO,this和作用域鏈也會因為某些語句而改變,后面會講到。
了解完Javascript的解析過程最后我們再來了解下firebug的控制臺對Javascript的報錯提示吧。
其實firebug的控制臺也算是JavaScript的解釋器,而且他們會提示我們哪行出現了錯誤或者錯誤發生在哪個時期,語法檢查階段錯誤,還是運行期錯誤。
如下:
alert(var);// SyntaxError: syntax error 語法分析階段錯誤 :語法錯誤
var=1;; // SyntaxError: missing variable name 語法分析階段錯誤 :var是保留字符,導致變量名丟失
a=b=v // ReferenceError: v is not defined 運行期錯誤: v 是未定義的
JavaScript錯誤信息)
有如此詳細的錯誤提示,是不是就很快就知道代碼中到底是哪里錯了呢!
接下來我們詳細來介紹執行上下文中的一個重要概念——作用域鏈。
作用域鏈(Scope Chain)
作用域鏈是處理標識符時進行變量查詢的變量對象列表,每個執行上下文都有自己的變量對象:對于全局上下文而言,其變量對象就是全局對象本身;對于函數而言,其變量對象就是活動對象。
作用域鏈以及執行上下文的關系
在Javascript中只有函數能規定作用域,全局執行上下文中的 Scope 是全局上下文中的屬性,也是最外層的作用域鏈。
函數的屬性[[Scope]]是在“預解析”的時候就已經存在的了,它包含了所有上層變量對象,并一直保存在函數中。就算函數永遠都沒被激活(調用),[[Scope]]也都還是存在函數對象上。
創建執行上下文的 Scope 屬性和進入執行上下文的過程如下:
Scope = AO + [[Scope]] //預解析時的 Scope 屬性
Scope = [AO].concat([[Scope]]); //執行階段,將AO添加到作用域鏈的最前端
執行上下文定義的 Scope 屬性變化過程
執行上下文中的[AO]是函數的活動對象,而[[Scope]]則是該函數屬性作用域。當前函數的AO永遠是在最前面的,保存在堆棧上,而每當函數激活的時候,這些AO都會壓棧到該堆棧上,查詢變量是先從棧頂開始查找,也就是說作用域鏈的棧頂永遠是當前正在執行的代碼所在環境的VO/AO(當函數調用結束后,則會從棧頂移除)。
通俗點講就是:JavaScript解釋器通過作用域鏈將不同執行位置上的變量對象串連成列表,并借助這個列表幫助JavaScript解釋器檢索變量的值。作用域鏈相當于一個索引表,并通過編號來存儲它們的嵌套關系。當JavaScript解釋器檢索變量的值,會按著這個索引編號進行快速查找,直到找到全局對象為止,如果沒有找到值,則傳遞一個特殊的 undefined值。
是不是又想到了一條JavaScript高效準則:為什么說在該函數內定義的變量,能減少函數嵌套能提高JavaScript的效率?因為函數定義的變量,此變量永遠在棧頂,這樣子查詢變量的時間變短了。
作用域的特性
保證查詢有序的訪問所有變量和函數
作用域鏈感覺就是一個VO鏈表,當訪問一個變量時,先在鏈表的第一個VO上查找,如果沒有找到則繼續在第二個VO上查找,直到搜索結束,也就是搜索到全局執行環境的VO中。這也就形成了作用域鏈的概念。
var color="blue";
function changecolor(){
var anothercolor="red";
function swapcolors(){
var tempcolor=anothercolor;
anothercolor=color;
color=tempcolor; // Todo something
}
swapcolors();
}
changecolor();//這里不能訪問tempcolor和anocolor;但是可以訪問color;
alert("Color is now "+color);
作用域鏈保護變量安全
函數的作用域是在函數創建即“預解析”階段就已經就已經定義了,而在代碼執行階段則是將函數的作用域添加到作用域鏈上。
原型鏈查詢
在介紹“預解析”階段時,我們有提到當創建函數時,同時也會創建原型鏈對象(prototype)函數天生的。原型鏈對象在作用域鏈中沒有找到變量對象時,那么就會通過原型鏈來查找。
function Foo() {
function bar() {
alert(x);
}
bar();
}
Object.prototype.x = 10;
Foo(); // 10
上例中在作用域鏈中遍歷查詢,到了全局對象了,該對象繼承自Object.prototype,因此,最終變量“x”的值就變成了10。不過,在原型鏈上定義變量對象有些瀏覽器不支持,譬如IE6,而且這樣增加了變量對象的查詢時間。所以變量聲明盡量在調用函數AO里,即在用到該變量的函數內聲明變量對象。
作用域是在“預解析”時就已經決定的,所以作用域被叫做靜態作用域,而在執行階段的則被叫做動態鏈,因為在執行階段會改變作用域鏈中填充的值。