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