傳統(tǒng)編譯語言的流程中痢虹,程序中的一段源代碼在執(zhí)行之前會經(jīng)歷三個步驟,統(tǒng)稱為“編譯”。
分詞/詞法分析(Tokenizing/Lexing)
這個過程會將由字符組成的字符串分解成(對編程語言來說)有意義的代碼塊,這些代碼塊被稱為詞法單元(token)。例如蓝纲,考慮程序var a = 2;
。這段程序通常會被分解成為下面這些詞法單元:var晌纫、a税迷、=、2
锹漱、;箭养。空格是否會被當作詞法單元哥牍,取決于空格在這門語言中是否具有意義毕泌。
分詞(tokenizing)和詞法分析(Lexing)之間的區(qū)別是非常微妙、晦澀的嗅辣,主要差異在于詞法單元的識別是通過有狀態(tài)還是無狀態(tài)的方式進行的撼泛。簡單來說,如果詞法單元生成器在判斷a
是一個獨立的詞法單元還是其他詞法單元的一部分時澡谭,調(diào)用的是有狀態(tài)的解析規(guī)則愿题,那么這個過程就被稱為詞法分析。
解析/語法分析(Parsing)
這個過程是將詞法單元流(數(shù)組)轉(zhuǎn)換成一個由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree潘酗,AST)杆兵。var a = 2;的抽象語法樹中可能會有一個叫作VariableDeclaration的頂級節(jié)點,接下來是一個叫作Identifier(它的值是a)的子節(jié)點仔夺,以及一個叫作AssignmentExpression的子節(jié)點琐脏。AssignmentExpression節(jié)點有一個叫作NumericLiteral(它的值是2)的子節(jié)點。
代碼生成
將AST轉(zhuǎn)換為可執(zhí)行代碼的過程被稱為代碼生成缸兔。這個過程與語言日裙、目標平臺等息息相關(guān)。拋開具體細節(jié)灶体,簡單來說就是有某種方法可以將var a = 2;的AST轉(zhuǎn)化為一組機器指令阅签,用來創(chuàng)建一個叫作a的變量(包括分配內(nèi)存等)掐暮,并將一個值儲存在a中蝎抽。
關(guān)于引擎如何管理系統(tǒng)資源超出了我們的討論范圍,因此只需要簡單地了解引擎可以根據(jù)需要創(chuàng)建并儲存變量即可路克。
比起那些編譯過程只有三個步驟的語言的編譯器樟结,JavaScript引擎要復雜得多。例如精算,在語法分析和代碼生成階段有特定的步驟來對運行性能進行優(yōu)化瓢宦,包括對冗余元素進行優(yōu)化等。
因此在這里只進行宏觀灰羽、簡單的介紹驮履,接下來你就會發(fā)現(xiàn)我們介紹的這些看起來有點高深的內(nèi)容與所要討論的事情有什么關(guān)聯(lián)。
首先廉嚼,JavaScript引擎不會有大量的(像其他語言編譯器那么多的)時間用來進行優(yōu)化玫镐,因為與其他語言不同,JavaScript的編譯過程不是發(fā)生在構(gòu)建之前的怠噪。
對于JavaScript來說恐似,大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微秒(甚至更短!)的時間內(nèi)傍念。在我們所要討論的作用域背后矫夷,JavaScript引擎用盡了各種辦法(比如JIT,可以延遲編譯甚至實施重編譯)來保證性能最佳憋槐。
簡單地說双藕,任何JavaScript代碼片段在執(zhí)行前都要進行編譯(通常就在執(zhí)行前)。因此阳仔,JavaScript編譯器首先會對var a = 2;
這段程序進行編譯忧陪,然后做好執(zhí)行它的準備,并且通常馬上就會執(zhí)行它。
總之:js執(zhí)行是先通過編譯器編譯好赤嚼,然后js引擎對其執(zhí)行
例如:處理var a = 2;
引擎
從頭到尾負責整個JavaScript程序的編譯及執(zhí)行過程旷赖。
編譯器
引擎的好朋友之一,負責語法分析及代碼生成等臟活累活(詳見前一節(jié)的內(nèi)容)更卒。
作用域
引擎的另一位好朋友等孵,負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢,并實施一套非常嚴格的規(guī)則蹂空,確定當前執(zhí)行的代碼對這些標識符的訪問權(quán)限俯萌。
當你看見var a = 2;這段程序時,很可能認為這是一句聲明上枕。但我們的新朋友引擎卻不這么看咐熙。事實上,引擎認為這里有兩個完全不同的聲明辨萍,一個由編譯器在編譯時處理棋恼,另一個則由引擎在運行時處理。
下面我們將var a = 2;分解锈玉,看看引擎和它的朋友們是如何協(xié)同工作的爪飘。
編譯器首先會將這段程序分解成詞法單元,然后將詞法單元解析成一個樹結(jié)構(gòu)拉背。但是當編譯器開始進行代碼生成時师崎,它對這段程序的處理方式會和預期的有所不同。
可以合理地假設(shè)編譯器所產(chǎn)生的代碼能夠用下面的偽代碼進行概括:“為一個變量分配內(nèi)存椅棺,將其命名為a犁罩,然后將值2保存進這個變量×骄危”然而床估,這并不完全正確。
事實上編譯器會進行如下處理鬼雀。
遇到var a顷窒,編譯器會詢問作用域是否已經(jīng)有一個該名稱的變量存在于同一個作用域的集合中。如果是源哩,編譯器會忽略該聲明鞋吉,繼續(xù)進行編譯;否則它會要求作用域在當前作用域的集合中聲明一個新的變量励烦,并命名為a谓着。
接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理a = 2這個賦值操作坛掠。引擎運行時會首先詢問作用域赊锚,在當前的作用域集合中是否存在一個叫作a的變量治筒。如果是,引擎就會使用這個變量舷蒲;如果不是耸袜,引擎會繼續(xù)查找該變量(查看1.3節(jié))。
如果引擎最終找到了a變量牲平,就會將2賦值給它堤框。否則引擎就會舉手示意并拋出一個異常!
總結(jié):變量的賦值操作會執(zhí)行兩個動作纵柿,首先編譯器會在當前作用域中聲明一個變量(如果之前沒有聲明過)蜈抓,然后在運行時引擎會在作用域中查找該變量,如果能夠找到就會對它賦值昂儒。
編譯器:
編譯器在編譯過程的第二步中生成了代碼沟使,引擎執(zhí)行它時,會通過查找變量a來判斷它是否已聲明過渊跋。查找的過程由作用域進行協(xié)助腊嗡,但是引擎執(zhí)行怎樣的查找,會影響最終的查找結(jié)果刹枉。引擎有兩種查找方式LHS/RHS(左右查找)
在我們的例子中叽唱,引擎會為變量a進行LHS查詢。另外一個查找的類型叫作RHS微宝。
LHS:左查找,就是在從等號的左邊進行查找虎眨,意思就是說我們要給查詢到的變量賦值蟋软,首先得查詢是否申明了這個變量,然后才能賦值的吧嗽桩!這時候用到的查詢就是從左邊查詢(LHS)(賦值操作)岳守,理解為賦值操作的目標。
LHS查詢比較松散碌冶,如果查詢不到湿痢,就會創(chuàng)建一個全局的,不會拋出異常
RHS:右查詢就是我們要獲得這個變量的值扑庞,查找變量是否存在譬重,然后獲取到值。理解為賦值操作的源頭罐氨。
左邊需要被賦值的是LHS查找臀规,右邊需要找到他的值然后進行操作的是RHS查找。