摘要:作用域鏈用于表明上下文的執(zhí)行順序。當(dāng)前上下文執(zhí)行完畢則出棧,執(zhí)行下一個(gè)上下文。
從一個(gè)簡單的例子出發(fā)
先從一個(gè)簡單的例子出發(fā)(先不涉及異步),看看自己是否大致了解瀏覽器的執(zhí)行機(jī)制:
console.log(a); var a=1; function foo(a){ console.log(a); var a=2; console.log(a); } foo(a);
undefined 1 2
如果你的預(yù)測結(jié)果不一樣,那你可以看看下面幾個(gè)常見的誤區(qū):
var a=1不是進(jìn)行了變量提升?為什么第一個(gè)打印的結(jié)果為 undefined?
答:變量提升只提升變量的聲明,并不進(jìn)行賦值。其中變量提升發(fā)生在預(yù)編譯階段,此時(shí)a=undefined,預(yù)編譯結(jié)束后代碼如下
//函數(shù)聲明和變量聲明進(jìn)行提升,且函數(shù)聲明優(yōu)先級(jí)更高 function foo(a){ console.log(a); var a=2; console.log(a); } var a; console.log(a); a=1; foo(a);
很明顯第一個(gè)結(jié)果為undefined。
foo函數(shù)中參數(shù)名和變量名都為a,使用時(shí)該以哪一個(gè)a為準(zhǔn)?
變量聲明在順序上跟在函數(shù)聲明和形式參數(shù)聲明之后,同時(shí),如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會(huì)干擾已經(jīng)存在的這類屬性。例子中的var a=2,可以拆分為var a;a=2;其中a=2是對參數(shù)a進(jìn)行賦值。現(xiàn)在我們詳細(xì)地說一說js的執(zhí)行機(jī)制:
首先你需要理解如下幾個(gè)概念:
JavaScript中的堆和棧棧(stack) 棧stack為自動(dòng)分配的內(nèi)存空間,它由系統(tǒng)自動(dòng)釋放;
堆(heap) 堆heap是動(dòng)態(tài)分配的內(nèi)存,大小不定也不會(huì)自動(dòng)釋放。
JavaScript 中的變量分為基本類型和引用類型。其中,基本類型存在于棧中,引用類型存在于堆中。在js的執(zhí)行階段,當(dāng)執(zhí)行到a=2這樣的賦值語句時(shí),js引擎線程會(huì)先判斷2是基本類型還是引用類型,如果它是基本類型,則直接對執(zhí)行棧中的AO進(jìn)行賦值a=2(AO會(huì)在下面的執(zhí)行上下文中講到),若是引用類型,則在堆中存入2,然后用2在堆中的地址對AO進(jìn)行賦值。執(zhí)行環(huán)境
js的執(zhí)行環(huán)境分為三種:
全局環(huán)境(JS代碼加載完畢后,進(jìn)入代碼預(yù)編譯即進(jìn)入全局環(huán)境)
函數(shù)環(huán)境(函數(shù)調(diào)用執(zhí)行時(shí),進(jìn)入該函數(shù)環(huán)境,不同的函數(shù)則函數(shù)環(huán)境不同)
eval(不建議使用,會(huì)有安全,性能等問題)
js每進(jìn)入一個(gè)執(zhí)行環(huán)境就會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,并將它放入執(zhí)行棧中。執(zhí)行上下文會(huì)在下文講到。
單線程(同步和異步)js是一門單線程語言,但并不意味著參與js執(zhí)行過程的線程就只有一個(gè)。一個(gè)有四個(gè)線程參與該過程:
JS引擎線程、事件觸發(fā)線程、定時(shí)器觸發(fā)線程、HTTP異步請求線程。其中,只有JS引擎線程在執(zhí)行JS腳本程序,其他三個(gè)線程只負(fù)責(zé)將滿足觸發(fā)條件的處理函數(shù)推進(jìn)事件隊(duì)列,等待JS引擎線程執(zhí)行。
舉一個(gè)簡單的例子來說:
console.log("script start"); setTimeout(function() { console.log("setTimeout"); }, 0); console.log("script end");
JS引擎主線程按代碼順序執(zhí)行,當(dāng)執(zhí)行到console.log("script start");,JS引擎主線程認(rèn)為該任務(wù)是同步任務(wù),所以立刻執(zhí)行輸出script start,然后繼續(xù)向下執(zhí)行;
JS引擎主線程執(zhí)行到setTimeout(function() { console.log("setTimeout"); }, 0);,JS引擎主線程認(rèn)為setTimeout是異步任務(wù)API,則向?yàn)g覽器內(nèi)核進(jìn)程申請開啟定時(shí)器線程進(jìn)行計(jì)時(shí)和控制該setTimeout任務(wù)。由于W3C在HTML標(biāo)準(zhǔn)中規(guī)定setTimeout低于4ms的時(shí)間間隔算為4ms,那么當(dāng)計(jì)時(shí)到4ms時(shí),定時(shí)器線程就把該回調(diào)處理函數(shù)推進(jìn)任務(wù)隊(duì)列中等待主線程執(zhí)行,然后JS引擎主線程繼續(xù)向下執(zhí)行;
JS引擎主線程執(zhí)行到console.log("script end");,JS引擎主線程認(rèn)為該任務(wù)是同步任務(wù),所以立刻執(zhí)行輸出script end;
JS引擎主線程上的任務(wù)執(zhí)行完畢(輸出script start和script end)后,主線程空閑,則開始讀取任務(wù)隊(duì)列中的事件任務(wù),將該任務(wù)隊(duì)里的事件任務(wù)推進(jìn)主線程中,按任務(wù)隊(duì)列順序執(zhí)行,最終輸出setTimeout,所以輸出的結(jié)果順序?yàn)閟cript start script end setTimeout;
如果還不清楚,可以看看下圖:
首先,這是一個(gè)瀏覽器環(huán)境,其中主線程操作堆和執(zhí)行棧,而RunTime中存在著許多web API,當(dāng)主線程讀取到setTimeOut等API時(shí),它會(huì)交給其他線程來處理(setTimeOut則是定時(shí)器觸發(fā)線程),定時(shí)器觸發(fā)線程會(huì)先將setTimeOut中的回調(diào)函數(shù)存放在event table中,當(dāng)滿足觸發(fā)條件時(shí)(如上面的4ms),就將回調(diào)函數(shù)推入事件隊(duì)列(callback queue)中,等待主線程空閑(執(zhí)行棧中為空),回調(diào)函數(shù)則被推入執(zhí)行棧中進(jìn)行執(zhí)行。
執(zhí)行上下文可理解為當(dāng)前的執(zhí)行環(huán)境,與該運(yùn)行環(huán)境相對應(yīng)。js引擎每進(jìn)入一個(gè)環(huán)境就會(huì)創(chuàng)建相應(yīng)的執(zhí)行上下文,創(chuàng)建執(zhí)行上下文的過程中,主要做了以下三件事件,如圖:
其中,變量對象VO(Variable object)用于存放聲明后的變量、函數(shù)和形參。我們舉一個(gè)例子來說:
var a = 10; function test(x) { var b = 20; }; test(30);
對應(yīng)的變量對象是:
// 全局上下文的變量對象 VO(global) = { a: undefined, test:// 是test函數(shù)位于堆中的地址 }; // test函數(shù)上下文的變量對象 VO(test) = { arguments: { x:undefined, length:1 }, b: undefined };
當(dāng)預(yù)編譯結(jié)束,js進(jìn)入解釋執(zhí)行階段時(shí),VO就會(huì)轉(zhuǎn)化為AO(Active object),也就是活動(dòng)對象。AO中變量和參數(shù)的值不再是undefined,它們的值會(huì)隨著js的逐步執(zhí)行而發(fā)生變化。
作用域鏈用于表明上下文的執(zhí)行順序。上例中的作用域鏈為:
scopeChain: [VO(test), AO(global)],
我們這里直接使用數(shù)組表示作用域鏈,作用域鏈的活動(dòng)對象或變量對象可以直接理解為作用域;
它的第一項(xiàng)永遠(yuǎn)是當(dāng)前作用域(當(dāng)前上下文的變量對象或活動(dòng)對象);
最后一項(xiàng)永遠(yuǎn)是全局作用域(全局執(zhí)行上下文的活動(dòng)對象);
作用域鏈保證了變量和函數(shù)的有序訪問,查找方式是沿著作用域鏈從左至右查找變量或函數(shù),找到則會(huì)停止查找,找不到則一直查找到全局作用域,再找不到則會(huì)拋出引用錯(cuò)誤。
this指向當(dāng)前作用域。這里不做過多分析。
執(zhí)行的三個(gè)階段js的執(zhí)行分為三個(gè)階段:
語法分析階段: