在這篇文章里,我將深入研究JavaScript中最基本的部分——執(zhí)行上下文(execution context)。讀完本文后,你應(yīng)該清楚了解解釋器做了什么,為什么函數(shù)和變量能在聲明前使用以及他們的值是如何決定的。
當(dāng)JavaScript代碼運(yùn)行,執(zhí)行環(huán)境非常重要,有下面幾種不同的情況:
在網(wǎng)上你能讀到許多關(guān)于作用域(scope)的資源,本文的目的是讓事情變得更簡單,讓我們將術(shù)語執(zhí)行上下文想象為當(dāng)前被執(zhí)行代碼的環(huán)境/作用域。說的夠多了,現(xiàn)在讓我們看一個包含全局和函數(shù)上下文的代碼例子。
很簡單的例子,我們有一個被紫色邊框圈起來的全局上下文和三個分別被綠色,藍(lán)色和橘色框起來的不同函數(shù)上下文。只有全局上下文(的變量)能被其他任何上下文訪問。
你可以有任意多個函數(shù)上下文,每次調(diào)用函數(shù)創(chuàng)建一個新的上下文,會創(chuàng)建一個私有作用域,函數(shù)內(nèi)部聲明的任何變量都不能在當(dāng)前函數(shù)作用域外部直接訪問。在上面的例子中,函數(shù)能訪問當(dāng)前上下文外面的變量聲明,但在外部上下文不能訪問內(nèi)部的變量/函數(shù)聲明。為什么會發(fā)生這種情況?代碼到底是如何被解釋的?
瀏覽器里的JavaScript解釋器被實(shí)現(xiàn)為單線程。這意味著同一時(shí)間只能發(fā)生一件事情,其他的行文或事件將會被放在叫做執(zhí)行棧里面排隊(duì)。下面的圖是單線程棧的抽象視圖:
我們已經(jīng)知道,當(dāng)瀏覽器首次載入你的腳本,它將默認(rèn)進(jìn)入全局執(zhí)行上下文。如果,你在你的全局代碼中調(diào)用一個函數(shù),你程序的時(shí)序?qū)⑦M(jìn)入被調(diào)用的函數(shù),并穿件一個新的執(zhí)行上下文,并將新創(chuàng)建的上下文壓入執(zhí)行棧的頂部。
如果你調(diào)用當(dāng)前函數(shù)內(nèi)部的其他函數(shù),相同的事情會在此上演。代碼的執(zhí)行流程進(jìn)入內(nèi)部函數(shù),創(chuàng)建一個新的執(zhí)行上下文并把它壓入執(zhí)行棧的頂部。瀏覽器將總會執(zhí)行棧頂?shù)膱?zhí)行上下文,一旦當(dāng)前上下文函數(shù)執(zhí)行結(jié)束,它將被從棧頂彈出,并將上下文控制權(quán)交給當(dāng)前的棧。下面的例子顯示遞歸函數(shù)的執(zhí)行棧調(diào)用過程:
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
這代碼調(diào)用自己三次,每次給i的值加一。每次foo函數(shù)被調(diào)用,將創(chuàng)建一個新的執(zhí)行上下文。一旦上下文執(zhí)行完畢,它將被從棧頂彈出,并將控制權(quán)返回給下面的上下文,直到只剩全局上下文能為止。
有5個需要記住的關(guān)鍵點(diǎn),關(guān)于執(zhí)行棧(調(diào)用棧):
我們現(xiàn)在已經(jīng)知道沒次調(diào)用函數(shù),都會創(chuàng)建新的執(zhí)行上下文。然而,在JavaScript解釋器內(nèi)部,每次調(diào)用執(zhí)行上下文,分為兩個階段:
可以將每個執(zhí)行上下文抽象為一個對象并有三個屬性:
executionContextObj = {
scopeChain: { /* 變量對象(variableObject)+ 所有父執(zhí)行上下文的變量對象*/ },
variableObject: { /*函數(shù) arguments/參數(shù),內(nèi)部變量和函數(shù)聲明 */ },
this: {}
}
當(dāng)函數(shù)被調(diào)用是executionContextObj被創(chuàng)建,但在實(shí)際函數(shù)執(zhí)行之前。這是我們上面提到的第一階段,創(chuàng)建階段。在此階段,解釋器掃描傳遞給函數(shù)的參數(shù)或arguments,本地函數(shù)聲明和本地變量聲明,并創(chuàng)建executionContextObj對象。掃描的結(jié)果將完成變量對象的創(chuàng)建。
下面是解釋器如果執(zhí)行代碼的偽邏輯:
讓我們看一個例子:
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
當(dāng)調(diào)用foo(22)時(shí),創(chuàng)建狀態(tài)像下面這樣:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
真如你看到的,創(chuàng)建狀態(tài)負(fù)責(zé)處理定義屬性的名字,不為他們指派具體的值,以及形參/實(shí)參的處理。一旦創(chuàng)建階段完成,執(zhí)行流進(jìn)入函數(shù)并且激活/代碼執(zhí)行階段,看下函數(shù)執(zhí)行完成后的樣子:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
你能在網(wǎng)上找到很多定義JavaScript hoisting術(shù)語的資源,解釋變量和函數(shù)聲明被提升到函數(shù)作用域的頂部。然而,沒有人解釋為什么會發(fā)生這種情況的細(xì)節(jié),學(xué)習(xí)了上面關(guān)于解釋器如何創(chuàng)建愛你活動對象的新知識,很容易明白為什么。看下面的例子:
(function() {
console.log(typeof foo); // 函數(shù)指針
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
}()); ? 我們能回答下面的問題:
希望現(xiàn)在你了解JavaScript解釋器如何執(zhí)行你的代碼。了解執(zhí)行上下文和堆棧,將有助于你了解背后的原因——為什么你的代碼被解釋為和你最初希望不同的值。
你想知道解釋器內(nèi)部的運(yùn)作的開銷太大,或者你的JavaScript知識的必要性?知道執(zhí)行上下文相幫你寫出更好的JavaScript?
你想知道解釋器的內(nèi)部工作原理,需要太多篇幅,和必要的JavaScript知識。知道執(zhí)行上下文能幫你寫出更好的JavaScript代碼。
注意:有些人一直在問閉包,回調(diào),延時(shí)等問題,我將在下一篇文章里提到,更多關(guān)注域執(zhí)行上下文有關(guān)的作用域鏈相關(guān)方面。
原文:http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/
更多建議: