文/何其甚
寫這篇小文之時(shí)找了好多參考資料,其中就有阮一峰的《JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop》和網(wǎng)友轉(zhuǎn)載的《【樸靈評(píng)注】JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop》,其中具體細(xì)節(jié)有不同理解有不同,但應(yīng)該并不妨礙整體運(yùn)行機(jī)制的理解。
一、運(yùn)行環(huán)境
JavaScript是伴隨著瀏覽器的誕生而誕生,所以JavaScript的執(zhí)行最多還是在瀏覽器環(huán)境之內(nèi)。但是JavaScript作為服務(wù)端腳本的概念在誕生之初就有,1995年網(wǎng)景公司就提出了服務(wù)端JavaScript的概念,并研發(fā)了 Netscape Enterprise Server;1996年微軟發(fā)布的JScript也可以運(yùn)行在服務(wù)端。隨著技術(shù)的發(fā)展各種JavaScript引擎出現(xiàn),2009年5月Node.js的發(fā)布將JavaScript作為服務(wù)端腳本推向了一個(gè)高潮。關(guān)于JavaScript服務(wù)端的實(shí)現(xiàn)可以參看wikipedia,https://en.wikipedia.org/wiki/List_of_server-side_JavaScript_implementations。
JavaScript的運(yùn)行不像C語(yǔ)言等其他編譯型語(yǔ)言編譯后直接在操作系統(tǒng)上運(yùn)行,因?yàn)樗悄_本語(yǔ)言,運(yùn)行時(shí)必須要借助引擎(解釋器)來(lái)運(yùn)行,所以它可以在封裝了引擎的環(huán)境下運(yùn)行。封裝了JavaScript引擎的環(huán)境可以分為兩類,一類是瀏覽器環(huán)境;一類是非瀏覽器環(huán)境,比如Node.js、MongoDB。我沒(méi)有采用wikipedia中clent-side和server-side的直接翻譯,因?yàn)镴avaScript既可以編寫服務(wù)端腳本也可以編寫shell腳本,甚至圖形界面應(yīng)用程序。
把運(yùn)行環(huán)境分為瀏覽器環(huán)境和非瀏覽器環(huán)境是因?yàn)樗麄兲峁┝私厝徊煌牟僮髂K。瀏覽器環(huán)境下JavaScript由三部分組成,分別是ECMAScript、DOM和BOM,BOM和DOM是針對(duì)瀏覽器環(huán)境所擴(kuò)展的操作方法。非瀏覽器環(huán)境,比如Node.js,也是以ECMAScript為基礎(chǔ),擴(kuò)展出了I/O操作、文件操作、數(shù)據(jù)庫(kù)操作等等;在MongoDB中則是可以作為shell腳本操作數(shù)據(jù)庫(kù);在Eclipse e4中可以編寫擴(kuò)展。
二、運(yùn)行機(jī)制
了解了JavaScript的運(yùn)行環(huán)境,我們來(lái)看看運(yùn)行機(jī)制。這里我們不再談微軟的JScript,一方面寫本文時(shí)我沒(méi)有找到詳盡的介紹JScript的資料,另一方面JScript的應(yīng)用現(xiàn)在不常見。
JavaScript是個(gè)什么樣子,取決于它初始應(yīng)用于哪里,它是作為瀏覽器的腳本出現(xiàn),主要用途是解決網(wǎng)頁(yè)中的用戶交互。頁(yè)面中的用戶交互行為會(huì)讓頁(yè)面中的DOM元素產(chǎn)生變化,比如用戶輸入信息后的反饋提示等等。JavaScript在瀏覽器環(huán)境中操作DOM,為避免復(fù)雜的同步問(wèn)題,決定了它采用單線程。如果同時(shí)有多個(gè)線程,有的在DOM節(jié)點(diǎn)上添加內(nèi)容,有的修改了整個(gè)節(jié)點(diǎn),甚至有的刪除了整個(gè)節(jié)點(diǎn),這個(gè)時(shí)候很難判斷到底采用哪個(gè)線程的結(jié)果。
JavaScript最大的特點(diǎn)就是單線程,在瀏覽器環(huán)境中中是,在非瀏覽器環(huán)境中同樣也是。單線程也就意味著JavaScript在同一時(shí)間只能進(jìn)行一項(xiàng)任務(wù),如果有多項(xiàng)任務(wù)的話,需要對(duì)任務(wù)進(jìn)行排隊(duì),完成一個(gè)才能繼續(xù)下一個(gè)。
不同的瀏覽器、不同的引擎、不同的執(zhí)行環(huán)境,執(zhí)行JavaScript的細(xì)節(jié)會(huì)有差異,但是不變的是單線程和隊(duì)列。
三、運(yùn)行過(guò)程
在瀏覽器環(huán)境中,JavaScript引擎按<script>標(biāo)簽代碼塊從上到下的順序加載并立即解釋執(zhí)行。
我們?cè)谶@里不探究引擎的詳盡解釋執(zhí)行細(xì)節(jié),比如詞法分析、語(yǔ)法分析以及語(yǔ)法樹的構(gòu)造等等,只說(shuō)它解釋執(zhí)行過(guò)程中非常重要的兩個(gè)時(shí)期預(yù)編譯期(預(yù)解析期)和執(zhí)行期。理解這兩個(gè)階段十分有助于理解JavaScript中的一些“奇特”的現(xiàn)象。
在預(yù)編譯期JavaScript會(huì)對(duì)var和function的聲明在其所在作用域內(nèi)進(jìn)行提升,提升的位置相當(dāng)于所在作用域開始位置。預(yù)編譯期需要注意下面幾個(gè)問(wèn)題:
1.預(yù)編譯首先是全局預(yù)編譯,函數(shù)體在未調(diào)用時(shí)不進(jìn)行預(yù)編譯
2.只有var和function聲明會(huì)提升
3.注意是在所在作用域內(nèi)提升,不會(huì)擴(kuò)展到其他作用域
4.預(yù)編譯后順序執(zhí)行
先看var變量聲明。以下示例在firefox中測(cè)試運(yùn)行。
console.log(a);//undefined
var a = 1;
console.log(a);//1
代碼中第一輸出的undefined代表的意思是變量已經(jīng)存在,只是沒(méi)有初始化。這段代碼預(yù)解析的等價(jià)結(jié)果是:
var a ;
console.log(a);
a = 1;
console.log(a);
再來(lái)看看非var變量的定義,全局變量定義。
console.log(a);//ReferenceError: a is not defined
a = 1;
console.log(a);
在這里可以看到var定義和非var定義的區(qū)別,在未定義之前調(diào)用提示變量沒(méi)有定義。
再來(lái)看let變量定義。
console.log(a);//ReferenceError: can't access lexical declaration `a' before initialization
let a = 1;
console.log(a);
let定義之前調(diào)用變量,firefox的錯(cuò)誤提示很明確:在聲明之前不能調(diào)用。
接下來(lái)看函數(shù)function的定義。
foo();//this is function foo
function foo(){
console.log("this is function foo");
}
在定義之前調(diào)用函數(shù),在許多語(yǔ)言中是錯(cuò)誤的,但是在JavaScript中它卻是正確的,執(zhí)行了在后面定義的函數(shù),這其實(shí)就預(yù)編譯其的函數(shù)聲明提前,上面這段相當(dāng)于下面這段代碼:
function foo(){
console.log("this is function foo");
}
foo();
JavaScript中定義函數(shù)還有另一種使用變量的方式,結(jié)合上面說(shuō)到的var變量聲明預(yù)編譯前置,可以理解下面這段代碼的執(zhí)行結(jié)果:
console.log(foo);//undefined
foo();//TypeError: foo is not a function
var foo = function (){
console.log("this is var foo");
}
可以看出來(lái)foo的聲明被前置,但是沒(méi)有初始化,所以foo的值是undefined,自然它也就不是函數(shù)。
console.log(foo);//
var foo = function (){
console.log("this is var foo");
}
foo();//this is var foo
函數(shù)有兩種常用定義方式var和function,兩種方式在預(yù)編譯期都會(huì)前置,但到底哪一種優(yōu)先生效呢?看下面的代碼。
foo();//this is function foo
var foo = function (){
console.log("this is var foo");
}
function foo(){
console.log("this is function foo");
}
foo();//this is var foo
利用我們上面的前置規(guī)則,我們來(lái)整理下思路。第一行的foo執(zhí)行的是function定義的函數(shù),最后的foo執(zhí)行的是var定義的函數(shù),那么它的等價(jià)順序應(yīng)該是這樣的:
function foo(){
console.log("this is function foo");
}
var foo ;
foo();
foo = function (){
console.log("this is var foo");
}
foo();
等價(jià)的順序中你可能會(huì)疑惑var foo的位置。首先確定一點(diǎn)是var聲明一定是前置的,function定義也是前置的,它們兩者都會(huì)前置到調(diào)用之前,也就是第一次調(diào)用foo()之前。至于var foo和function的前后位置它們兩個(gè)互換是等價(jià)的,無(wú)論var foo在function之前還是之后都是一樣的。
下面我們?cè)賮?lái)看下函數(shù)體內(nèi)的預(yù)編譯情況。
console.log(a);//ReferenceError: a is not defined
function foo(){
var a = 1;
}
fcc();//ReferenceError: fcc is not defined
function foo(){
function fcc(){}
}
函數(shù)體內(nèi)的聲明不會(huì)前置到外部作用域。要注意一點(diǎn)就是函數(shù)體的預(yù)解析發(fā)生在函數(shù)被調(diào)用之時(shí),被調(diào)用時(shí)先進(jìn)行函數(shù)體的預(yù)編譯,然后按順序進(jìn)行執(zhí)行。
參考內(nèi)容:
https://en.wikipedia.org/wiki/JavaScript#Server-side_JavaScript
https://en.wikipedia.org/wiki/List_of_server-side_JavaScript_implementations