通過解析過程了解JavaScript

轉(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ù)制過來的信息如下:

  1. 內(nèi)部變量表varDecls:varDecls保存的用var進(jìn)行顯式聲明的局部變量。
  2. 內(nèi)嵌函數(shù)表funDecls:在“預(yù)解析”階段,發(fā)現(xiàn)有函數(shù)定義的時(shí)候,除了記錄函數(shù)的聲明外,還會(huì)創(chuàng)建一個(gè)原型鏈對(duì)象(prototype)。
  3. …其他的信息。

執(zhí)行上下文(execution context)

(一)預(yù)解析階段創(chuàng)建的執(zhí)行上下文包括:變量對(duì)象、作用域鏈、this

  1. 變量對(duì)象(Variable Object):由var declaration、function declaration(變量聲明、函數(shù)聲明)、arguments(參數(shù))構(gòu)成。變量對(duì)象是以單例形式存在。
  2. 作用域鏈(Scope Chain):variable object + all parent scopes(變量對(duì)象以及所有父級(jí)作用域)構(gòu)成。
  3. 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ù)值。

  1. 函數(shù)的形參:執(zhí)行上下文的變量對(duì)象的一個(gè)屬性,其屬性名就是形參的名字,其值就是實(shí)參的值;對(duì)于沒有傳遞的參數(shù),其值為undefined。
  2. 函數(shù)聲明:執(zhí)行上下文的變量對(duì)象的一個(gè)屬性,屬性名和值都是函數(shù)對(duì)象創(chuàng)建出來的;如果變量對(duì)象已經(jīng)包含了相同名字的屬性,則會(huì)替換它的值。
  3. 變量聲明:執(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ì)改變作用域鏈中填充的值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目錄 1.靜態(tài)作用域與動(dòng)態(tài)作用域 2.變量的作用域 3.JavaScript 中變量的作用域 4.JavaScri...
    一縷殤流化隱半邊冰霜閱讀 7,266評(píng)論 37 113
  • 繼承 一、混入式繼承 二、原型繼承 利用原型中的成員可以被和其相關(guān)的對(duì)象共享這一特性,可以實(shí)現(xiàn)繼承,這種實(shí)現(xiàn)繼承的...
    magic_pill閱讀 1,128評(píng)論 0 3
  • 1,javascript 基礎(chǔ)知識(shí) Array對(duì)象 Array對(duì)象屬性 Arrray對(duì)象方法 Date對(duì)象 Dat...
    Yuann閱讀 1,153評(píng)論 0 1
  • Caffe訓(xùn)練自己的數(shù)據(jù)集并用Python接口預(yù)測(cè) 本教程作者是清華大學(xué)在讀碩士金天童鞋,在當(dāng)?shù)剌^為英俊的男子,大...
    LucasJin閱讀 8,876評(píng)論 4 6
  • 七月二十五日 天氣晴 多云 有雨 好雨知時(shí)節(jié),當(dāng)春乃發(fā)生。倘若發(fā)生在酷熱難耐的奧斯汀的夏季,仍不失為一場(chǎng)好雨。...
    盼之閱讀 713評(píng)論 0 0

友情鏈接更多精彩內(nèi)容