執(zhí)行上下文和執(zhí)行棧
開始之前,我們先看以下代碼。
console.log(a)
// Uncaught ReferenceError: a is not defined
console.log(a)
// undefined
var a = 10
第一段代碼報錯很好理解,a 沒有聲明。所以拋出錯誤。
第二段代碼中 a 的聲明在使用 a 之后,打印 a 的值是 undefined。
也就是說在使用 a 的時候,a 已經(jīng)被聲明了。這就很奇怪了,明明 a 的聲明在這一行下面,為什么這個時候就已經(jīng)被聲明了呢?
其實這就是變量提升的概念。本質(zhì)上是因為當(dāng)代碼真正執(zhí)行之前就已經(jīng)做了一些準(zhǔn)備工作。而這些工作跟執(zhí)行上下文有著緊密的聯(lián)系,我們需要先來了解什么是執(zhí)行上下文。
執(zhí)行上下文
簡單來說執(zhí)行上下文(Execution Context)就是執(zhí)行代碼的環(huán)境。所有的代碼都在執(zhí)行上下文中執(zhí)行。
上面的例子都是在全局上下文中執(zhí)行的,其實執(zhí)行上下文可以分為下面這三種
- 全局執(zhí)行上下文 (Global Execution Context)
- 這是最基礎(chǔ)或者默認(rèn)的執(zhí)行上下文,是代碼一開始運行就會創(chuàng)建的上下文。
- 一個程序中只會有一個全局執(zhí)行上下文
- 所有不在函數(shù)內(nèi)部的代碼都在全局執(zhí)行上下文之中
- 函數(shù)執(zhí)行上下文 (Functional Execution Context)
- 當(dāng)一個函數(shù)被調(diào)用時, 會為該函數(shù)創(chuàng)建一個上下文
- 每個函數(shù)都有自己的執(zhí)行上下文
- Eval 函數(shù)執(zhí)行上下文 (Eval Function Execution Context)
- 執(zhí)行在 eval 函數(shù)內(nèi)部的代碼也會有它屬于自己的執(zhí)行上下文
下面有一個例子
var v = 'global_context'
function f1() {
var v1 = 'f1 context'
function f2() {
var v2 = 'f2 context'
function f3() {
var v3 = 'f3 context'
console.log(v3)
}
f3()
console.log(v2)
}
f2()
console.log(v1)
}
f1()
console.log(v)
最外側(cè)的是全局執(zhí)行上下文,它有 f1 和 v 這兩個變量,f1、f2、f3內(nèi)部是三個函數(shù)執(zhí)行上下文(Eval 函數(shù)執(zhí)行上下文不是很常用,在這里不做介紹)。
通過上面我們了解了每個函數(shù)都對應(yīng)一個執(zhí)行上下文,實際代碼中肯定會有很多的函數(shù),甚至函數(shù)會嵌套函數(shù),這些執(zhí)行上下文是如何組織起來的呢?代碼又是如何運行的呢?
其實這些都是執(zhí)行棧的工作。
執(zhí)行棧
執(zhí)行棧,其他語言中被稱為調(diào)用棧,與存儲變量的那個棧的概念不同,它是被用來存儲代碼運行時創(chuàng)建的所有執(zhí)行上下文的棧。
當(dāng) JavaScript 引擎第一次遇到你的腳本時,它會創(chuàng)建一個全局的執(zhí)行上下文并且壓入當(dāng)前執(zhí)行棧。每當(dāng)引擎遇到一個函數(shù)調(diào)用,它會為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文并壓入棧的頂部。
Javascript 是一門單線程的語言,這就意味著同一個時間只能處理一個任務(wù)。因此引擎只會執(zhí)行那些執(zhí)行上下文位于棧頂?shù)暮瘮?shù)。當(dāng)該函數(shù)執(zhí)行結(jié)束時,執(zhí)行上下文從棧中彈出,控制流程到達(dá)當(dāng)前棧中的下一個上下文。
我們在上面的代碼的執(zhí)行過程可以歸結(jié)為下面這個圖:
文字版總結(jié)如下:
- 全局上下文壓入棧頂
- 每執(zhí)行某一函數(shù)就為其創(chuàng)建一個執(zhí)行上下文,并壓入棧頂
- 棧頂?shù)暮瘮?shù)執(zhí)行完之后它的執(zhí)行上下文就會從執(zhí)行棧中彈出,將控制權(quán)交給下一個上下文
- 所有函數(shù)執(zhí)行完之后執(zhí)行棧中只剩下全局上下文,它會在應(yīng)用關(guān)閉時銷毀
執(zhí)行上下文的創(chuàng)建
如果執(zhí)行上下文抽象成為一個對象的話它是如下的對象
executionContextObj = {
'scopeChain': { /* 變量對象(variableObject)+ 所有父級執(zhí)行上下文的變量對象 */ },
'variableObject': { /* 函數(shù) arguments/參數(shù),內(nèi)部變量和函數(shù)聲明 */ },
'this': {}
}
其中 variableObject 不是一成不變的,按照時間順序可以分為 VO 和 AO
-
VO 變量對象(Variable Object)
- 它是執(zhí)行上下文中都有的對象。
- 執(zhí)行上下文中可被訪問但是不能被 delete 的函數(shù)標(biāo)示符、形參、變量聲明等都會被掛在這個對象上
- 對象的屬性名對應(yīng)它們的名字,對象屬性的值對應(yīng)它們的值。
- 該對象不能直接訪問到
-
AO 活動對象(Activation object)
- 當(dāng)函數(shù)開始執(zhí)行的時候,這個執(zhí)行上下文兒中的變量對象就被激活,這時候 VO 就變成了 AO
因此執(zhí)行上下文創(chuàng)建的具體過程如下:
- 找到當(dāng)前上下文調(diào)用函數(shù)的代碼
- 執(zhí)行代碼之前,先創(chuàng)建執(zhí)行上下文
- 創(chuàng)建階段:
- 創(chuàng)建變量對象:
- 創(chuàng)建 arguments 對象,和參數(shù)
- 掃描上下文的函數(shù)申明:
- 每掃描到一個函數(shù)什么就會用函數(shù)名創(chuàng)建一個屬性,它是一個指針,指向該函數(shù)在內(nèi)存中的地址
- 如果函數(shù)名已經(jīng)存在,對應(yīng)的屬性值會被新的指針覆蓋
- 掃描上下文的變量申明:
- 每掃描到一個變量就會用變量名作為屬性名,其值初始化為 undefined
- 如果該變量名在變量對象中已經(jīng)存在,則直接跳過繼續(xù)掃描
- 初始化作用域鏈
- 確定上下文中 this 的指向
- 創(chuàng)建變量對象:
- 代碼執(zhí)行階段
- 執(zhí)行函數(shù)體中的代碼,給變量賦值
注意:
- 全局上下文的變量對象初始化是全局對象
- 全局上下文的生命周期,與程序的生命周期一致,只要程序運行不結(jié)束,比如關(guān)掉瀏覽器窗口,全局上下文就會一直存在。
- 作用域鏈(scopeChain) 和 this 的指向我們后面再詳細(xì)了解
我們看一個例子
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
在調(diào)用了 foo(22) 的時候,創(chuàng)建階段如下所示
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
激活階段如下
fooExecutionContext = {
scopeChain: { ... },
activationObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
注意
創(chuàng)建需要注意以下幾點
- 創(chuàng)建階段的創(chuàng)建順序是:函數(shù)的形參聲明并賦值 ==>> 函數(shù)聲明 ==>> 變量聲明
- 創(chuàng)建階段處理函數(shù)重名和變量重名的策略不同,簡單來說就是函數(shù)優(yōu)先級高。
function foo(a){
console.log(a)
var a = 10
}
foo(20) // 20
function foo(a){
console.log(a)
function a(){}
}
foo(20) // ? a(){}
function foo(){
console.log(a)
var a = 10
function a(){}
}
foo() // f a(){}
變量提升
通過上面的介紹我們其實就知道了變量提升這一現(xiàn)象的出現(xiàn)的根本原因就是執(zhí)行上下文在創(chuàng)建的時候就會掃描上下文中的變量將其聲明出來,并設(shè)置為 VO 的屬性。
我們分析下面的代碼來加深印象。
(function() {
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
}());?
我們來回答以下問題
- 為什么我們能在 foo 聲明之前訪問它?
回想 VO 的創(chuàng)建階段,foo 在該階段就已經(jīng)被創(chuàng)建在變量對象中。因此可以訪問它。
- foo 被聲明了兩次, 為什么 foo 展現(xiàn)出來的是 functiton,而不是undefined 或者 string
在創(chuàng)建階段,函數(shù)聲明是優(yōu)先于變量被創(chuàng)建的。而且在變量的創(chuàng)建過程中,如果發(fā)現(xiàn) VO 中已經(jīng)存在相同名稱的屬性,則不會影響已經(jīng)存在的屬性。
因此,對 foo() 函數(shù)的引用首先被創(chuàng)建在活動對象里,并且當(dāng)我們解釋到 var foo 時,我們看見 foo 屬性名已經(jīng)存在,所以代碼什么都不做并繼續(xù)執(zhí)行。
- 為什么 bar 的值是 undefined?
bar 采用的是函數(shù)表達(dá)式的方式來定義的,所以 bar 實際上是一個變量,但變量的值是函數(shù),并且我們知道變量在創(chuàng)建階段被創(chuàng)建但他們被初始化為 undefined。