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