在這篇文章中甚侣,我會(huì)深入理解JavaScript最根本的組成之一 : "執(zhí)行環(huán)境(執(zhí)行上下文)"创千。文章結(jié)束后木柬,你應(yīng)該對(duì)解釋器試圖做什么皆串,為什么一些函數(shù)/變量在未聲明時(shí)就可以調(diào)用并且他們的值是如何確定的有一個(gè)清晰的認(rèn)識(shí)。
什么是執(zhí)行環(huán)境(執(zhí)行上下文)
當(dāng)代碼在JavaScript中運(yùn)行的時(shí)候眉枕,代碼在環(huán)境中被執(zhí)行是非常重要的恶复,它會(huì)被評(píng)估為以下之一類型來運(yùn)行:
全局代碼:默認(rèn)環(huán)境,你的代碼第一時(shí)間在這兒運(yùn)行齐遵。
函數(shù)代碼:當(dāng)執(zhí)行流進(jìn)入一個(gè)函數(shù)體的時(shí)候寂玲。
Eval代碼:在eval()函數(shù)中的文本。
你可以在網(wǎng)上查找關(guān)于作用域的大量資料梗摇,這篇文章的目的就是讓事情變得更容易理解拓哟。讓我們把執(zhí)行環(huán)境作為環(huán)境/作用域,當(dāng)前代碼被評(píng)估在這個(gè)環(huán)境/作用域中。現(xiàn)在伶授,讓我們來看一個(gè)例子断序,代碼被評(píng)估某個(gè)類型,這個(gè)例子中類型包括全局和函數(shù)環(huán)境:
這里并沒有什么特別的糜烹,我們有一個(gè)全局環(huán)境违诗,全局環(huán)境由紫色邊框表示,還有三個(gè)不同的函數(shù)環(huán)境分別由綠色邊框疮蹦,藍(lán)色邊框和橙色邊框表示诸迟。這里只能由一個(gè)全局環(huán)境,在你的程序中愕乎,全局環(huán)境可以被其他環(huán)境訪問阵苇。
你可以由很多的函數(shù)環(huán)境,每個(gè)函數(shù)都會(huì)創(chuàng)建一個(gè)新的函數(shù)環(huán)境感论,在新的函數(shù)環(huán)境中绅项,會(huì)創(chuàng)建一個(gè)私有作用域,在這個(gè)函數(shù)中創(chuàng)建的任何聲明都不能被當(dāng)前函數(shù)作用域之外的地方訪問比肄。在上面例子中快耿,一個(gè)函數(shù)可以訪問當(dāng)前環(huán)境外部定義的變量囊陡,但是在外部卻無法訪問函數(shù)內(nèi)部聲明的變量。為什么這樣掀亥?這段代碼究竟是如何評(píng)估的撞反?
執(zhí)行環(huán)境棧
JavaScript解釋器在瀏覽器中是單線程的,這意味著瀏覽器在同一時(shí)間內(nèi)只執(zhí)行一個(gè)事件搪花,對(duì)于其他的事件我們把它們排隊(duì)在一個(gè)稱為 執(zhí)行棧的地方痢畜。下表是一個(gè)單線程棧的抽象視圖。
我們已經(jīng)知道鳍侣,當(dāng)瀏覽器第一次加載你的script丁稀,它默認(rèn)的進(jìn)了全局執(zhí)行環(huán)境。如果在你的全局代碼中你調(diào)用了一個(gè)函數(shù)倚聚,那么順序流就會(huì)進(jìn)入到你調(diào)用的函數(shù)當(dāng)中线衫,創(chuàng)建一個(gè)新的執(zhí)行環(huán)境并且把這個(gè)環(huán)境添加到執(zhí)行棧的頂部。
如果你在當(dāng)前的函數(shù)中調(diào)用了其他函數(shù)惑折,同樣的事會(huì)再次發(fā)生授账。執(zhí)行流進(jìn)入內(nèi)部函數(shù),并且創(chuàng)建一個(gè)新的執(zhí)行環(huán)境惨驶,把它添加到已經(jīng)存在的執(zhí)行棧的頂部白热。瀏覽器始終執(zhí)行當(dāng)前在棧頂部的執(zhí)行環(huán)境。一旦函數(shù)完成了當(dāng)前的執(zhí)行環(huán)境粗卜,它就會(huì)被彈出棧的頂部, 把控制權(quán)返回給當(dāng)前執(zhí)行環(huán)境的下個(gè)執(zhí)行環(huán)境屋确。下面例子展示了一個(gè)遞歸函數(shù)和該程序的執(zhí)行棧:
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
這段代碼簡單地調(diào)用了自己三次,由1遞增i的值。每次函數(shù)foo被調(diào)用,一個(gè)新的執(zhí)行環(huán)境就會(huì)被調(diào)用幔嫂。一旦一個(gè)環(huán)境完成了執(zhí)行,它就會(huì)被彈出執(zhí)行棧并且把控制權(quán)返回給當(dāng)前執(zhí)行環(huán)境的下個(gè)執(zhí)行環(huán)境直到再次到達(dá)全局執(zhí)行環(huán)境刨啸。
記住執(zhí)行棧,這兒有五個(gè)關(guān)鍵點(diǎn)
- 單線程
- 同步執(zhí)行
- 一個(gè)全局環(huán)境
- 無限的函數(shù)環(huán)境
- 函數(shù)被調(diào)用就會(huì)創(chuàng)建一個(gè)新的執(zhí)行環(huán)境识脆,甚至調(diào)用自己设联。
執(zhí)行環(huán)境的詳情
現(xiàn)在我們知道,一個(gè)函數(shù)被調(diào)用就會(huì)創(chuàng)建一個(gè)新的執(zhí)行環(huán)境灼捂。然而解釋器的內(nèi)部离例,每次調(diào)用執(zhí)行環(huán)境會(huì)有兩個(gè)階段:
- 創(chuàng)建階段
- 當(dāng)函數(shù)被調(diào)用,但是為執(zhí)行內(nèi)部代碼之前:
- 創(chuàng)建一個(gè)作用域鏈纵东。
- 創(chuàng)建變量粘招,函數(shù)和參數(shù)啥寇。
- 確定this的值偎球。
- 激活/代碼執(zhí)行階段
- 賦值洒扎,引用函數(shù),解釋/執(zhí)行代碼衰絮。
這可能意味著每個(gè)執(zhí)行環(huán)境在概念上作為一個(gè)對(duì)象并帶有三個(gè)屬性
executionContextObj = {
scopeChain: { /* variableObject + all parent execution context's variableObject */ },
//作用域鏈:{變量對(duì)象+所有父執(zhí)行環(huán)境的變量對(duì)象}
variableObject: { /* function arguments / parameters, inner variable and function declarations */ },
//變量對(duì)象:{函數(shù)形參+內(nèi)部的變量+函數(shù)聲明(但不包含表達(dá)式)}
this: {}
}
活動(dòng)/變量 對(duì)象(AO/VO)
當(dāng)函數(shù)被調(diào)用袍冷,executionContextObj就被創(chuàng)建,該對(duì)象在實(shí)際函數(shù)執(zhí)行前就已創(chuàng)建猫牡。這就是已知的第一個(gè)階段創(chuàng)建階段.在第一階段胡诗,解釋器創(chuàng)建了executionContextObj對(duì)象,通過掃描函數(shù)淌友,傳遞形參煌恢,函數(shù)聲明和局部變量聲明。掃描的結(jié)果成為了變量對(duì)象在executionContextObj中震庭。
- 這有一個(gè)解釋器是如何評(píng)估代碼的偽概述:
- 找到一些代碼來調(diào)用函數(shù)
- 在執(zhí)行函數(shù)代碼前瑰抵,創(chuàng)建執(zhí)行環(huán)境
- 進(jìn)入創(chuàng)建階段:
- 初始化作用域鏈
- 創(chuàng)建變量對(duì)象:
- 創(chuàng)建arguments對(duì)象,檢查環(huán)境中的參數(shù)器联,初始化名和值二汛,創(chuàng)建一個(gè)參考副本
- 掃描環(huán)境中內(nèi)的函數(shù)聲明:
- 某個(gè)函數(shù)被發(fā)現(xiàn),在變量對(duì)象創(chuàng)建一個(gè)屬性拨拓,它是函數(shù)的確切名肴颊。它是一個(gè)指針在內(nèi)存中,指向這個(gè)函數(shù)渣磷。
- 如果這個(gè)函數(shù)名已存在婿着,這個(gè)指針的值將會(huì)重寫。
- 掃描環(huán)境內(nèi)的變量聲明
- 某個(gè)變量聲明被發(fā)現(xiàn)醋界,在變量對(duì)象中創(chuàng)建一個(gè)屬性祟身,他是變量的名,初始化它的值為undefined物独。
- 如果變量名在變量對(duì)象中已存在袜硫,什么也不做,繼續(xù)掃描挡篓。
- 在環(huán)境中確定this的值婉陷。
- 激活/代碼執(zhí)行階段:在當(dāng)前上下文上運(yùn)行/解釋函數(shù)代碼,并隨著代碼一行行執(zhí)行指派變量的值
看下面例子:
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
On calling foo(22), the creation stage looks as follows:
在調(diào)用foo(22)時(shí)官研,創(chuàng)建階段像下面這樣:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
正如你看到的秽澳,創(chuàng)建階段處理了定義屬性的名,但是并不把值賦給變量戏羽,不包括形參和實(shí)參担神。一旦創(chuàng)建階段完成,執(zhí)行流進(jìn)入函數(shù)并且激活/代碼執(zhí)行階段,在函數(shù)執(zhí)行結(jié)束之后,看起來像這樣:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
進(jìn)階一言
你可以在網(wǎng)上找到大量的術(shù)語來描述JavaScript進(jìn)階始花。解釋變量和函數(shù)聲明被提升到它們函數(shù)作用域的頂端妄讯。然而孩锡,沒有一個(gè)詳細(xì)的解釋為什么這樣, 現(xiàn)在你配備了關(guān)于解釋器怎么創(chuàng)建活動(dòng)對(duì)象的新知識(shí)亥贸,這會(huì)很明白這是為什么躬窜。看看下面例子:
?(function() {
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
}());?
現(xiàn)在我們能解答的問題有:
為什么在聲明foo之前我們就可以調(diào)用?
如果我們按照創(chuàng)建階段進(jìn)行炕置,我們知道變量在激活/執(zhí)行階段之前已經(jīng)被創(chuàng)建了荣挨。因此,在函數(shù)流開始執(zhí)行朴摊,foo已經(jīng)在活動(dòng)對(duì)象中被定義了默垄。
foo被聲明了兩次, 為什么foo展現(xiàn)出來的是functiton,而不是undefined或者string
我們從創(chuàng)建階段知道,盡管foo被聲明了兩次甚纲,函數(shù)在活動(dòng)對(duì)象中是在變量之前被創(chuàng)建的厕倍,并且如果屬性名在活動(dòng)對(duì)象已經(jīng)存在,我們會(huì)簡單地繞過這個(gè)聲明。
所以贩疙,引用函數(shù)foo()是在活動(dòng)對(duì)象上第一次被創(chuàng)建的讹弯, 當(dāng)我們解釋到 var foo的時(shí)候,我們發(fā)現(xiàn)屬性名foo已經(jīng)存在这溅,所以代碼不會(huì)做任何處理组民,只是繼續(xù)進(jìn)行
為什么bar是undefined?
bar確實(shí)是一個(gè)變量悲靴,并且值是一個(gè)函數(shù)臭胜。我們知道變量是在創(chuàng)建階段被創(chuàng)建的,但是它們的值被初始化為undefined癞尚。