執(zhí)行上下文和執(zhí)行棧
開始之前,我們先看以下代碼。
console.log(a)
// Uncaught ReferenceError: a is not defined
console.log(a)
// undefined
var a = 10
第一段代碼報(bào)錯(cuò)很好理解羽杰,a 沒有聲明。所以拋出錯(cuò)誤到推。
第二段代碼中 a 的聲明在使用 a 之后考赛,打印 a 的值是 undefined。
也就是說在使用 a 的時(shí)候莉测,a 已經(jīng)被聲明了颜骤。這就很奇怪了,明明 a 的聲明在這一行下面捣卤,為什么這個(gè)時(shí)候就已經(jīng)被聲明了呢忍抽?
其實(shí)這就是變量提升的概念。本質(zhì)上是因?yàn)楫?dāng)代碼真正執(zhí)行之前就已經(jīng)做了一些準(zhǔn)備工作董朝。而這些工作跟執(zhí)行上下文有著緊密的聯(lián)系鸠项,我們需要先來了解什么是執(zhí)行上下文。
執(zhí)行上下文
簡(jiǎn)單來說執(zhí)行上下文(Execution Context)就是執(zhí)行代碼的環(huán)境子姜。所有的代碼都在執(zhí)行上下文中執(zhí)行锈锤。
上面的例子都是在全局上下文中執(zhí)行的,其實(shí)執(zhí)行上下文可以分為下面這三種
- 全局執(zhí)行上下文 (Global Execution Context)
- 這是最基礎(chǔ)或者默認(rèn)的執(zhí)行上下文闲询,是代碼一開始運(yùn)行就會(huì)創(chuàng)建的上下文。
- 一個(gè)程序中只會(huì)有一個(gè)全局執(zhí)行上下文
- 所有不在函數(shù)內(nèi)部的代碼都在全局執(zhí)行上下文之中
- 函數(shù)執(zhí)行上下文 (Functional Execution Context)
- 當(dāng)一個(gè)函數(shù)被調(diào)用時(shí), 會(huì)為該函數(shù)創(chuàng)建一個(gè)上下文
- 每個(gè)函數(shù)都有自己的執(zhí)行上下文
- Eval 函數(shù)執(zhí)行上下文 (Eval Function Execution Context)
- 執(zhí)行在 eval 函數(shù)內(nèi)部的代碼也會(huì)有它屬于自己的執(zhí)行上下文
下面有一個(gè)例子
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 這兩個(gè)變量扭弧,f1、f2记舆、f3內(nèi)部是三個(gè)函數(shù)執(zhí)行上下文(Eval 函數(shù)執(zhí)行上下文不是很常用鸽捻,在這里不做介紹)。
通過上面我們了解了每個(gè)函數(shù)都對(duì)應(yīng)一個(gè)執(zhí)行上下文,實(shí)際代碼中肯定會(huì)有很多的函數(shù)御蒲,甚至函數(shù)會(huì)嵌套函數(shù)衣赶,這些執(zhí)行上下文是如何組織起來的呢?代碼又是如何運(yùn)行的呢厚满?
其實(shí)這些都是執(zhí)行棧的工作府瞄。
執(zhí)行棧
執(zhí)行棧,其他語言中被稱為調(diào)用棧碘箍,與存儲(chǔ)變量的那個(gè)棧的概念不同遵馆,它是被用來存儲(chǔ)代碼運(yùn)行時(shí)創(chuàng)建的所有執(zhí)行上下文的棧。
當(dāng) JavaScript 引擎第一次遇到你的腳本時(shí)丰榴,它會(huì)創(chuàng)建一個(gè)全局的執(zhí)行上下文并且壓入當(dāng)前執(zhí)行棧货邓。每當(dāng)引擎遇到一個(gè)函數(shù)調(diào)用,它會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并壓入棧的頂部四濒。
Javascript 是一門單線程的語言换况,這就意味著同一個(gè)時(shí)間只能處理一個(gè)任務(wù)。因此引擎只會(huì)執(zhí)行那些執(zhí)行上下文位于棧頂?shù)暮瘮?shù)盗蟆。當(dāng)該函數(shù)執(zhí)行結(jié)束時(shí)戈二,執(zhí)行上下文從棧中彈出,控制流程到達(dá)當(dāng)前棧中的下一個(gè)上下文姆涩。
我們?cè)谏厦娴拇a的執(zhí)行過程可以歸結(jié)為下面這個(gè)圖:
文字版總結(jié)如下:
- 全局上下文壓入棧頂
- 每執(zhí)行某一函數(shù)就為其創(chuàng)建一個(gè)執(zhí)行上下文挽拂,并壓入棧頂
- 棧頂?shù)暮瘮?shù)執(zhí)行完之后它的執(zhí)行上下文就會(huì)從執(zhí)行棧中彈出,將控制權(quán)交給下一個(gè)上下文
- 所有函數(shù)執(zhí)行完之后執(zhí)行棧中只剩下全局上下文骨饿,它會(huì)在應(yīng)用關(guān)閉時(shí)銷毀
執(zhí)行上下文的創(chuàng)建
如果執(zhí)行上下文抽象成為一個(gè)對(duì)象的話它是如下的對(duì)象
executionContextObj = {
'scopeChain': { /* 變量對(duì)象(variableObject)+ 所有父級(jí)執(zhí)行上下文的變量對(duì)象 */ },
'variableObject': { /* 函數(shù) arguments/參數(shù)亏栈,內(nèi)部變量和函數(shù)聲明 */ },
'this': {}
}
其中 variableObject 不是一成不變的,按照時(shí)間順序可以分為 VO 和 AO
-
VO 變量對(duì)象(Variable Object)
- 它是執(zhí)行上下文中都有的對(duì)象宏赘。
- 執(zhí)行上下文中可被訪問但是不能被 delete 的函數(shù)標(biāo)示符绒北、形參、變量聲明等都會(huì)被掛在這個(gè)對(duì)象上
- 對(duì)象的屬性名對(duì)應(yīng)它們的名字察署,對(duì)象屬性的值對(duì)應(yīng)它們的值闷游。
- 該對(duì)象不能直接訪問到
-
AO 活動(dòng)對(duì)象(Activation object)
- 當(dāng)函數(shù)開始執(zhí)行的時(shí)候,這個(gè)執(zhí)行上下文兒中的變量對(duì)象就被激活贴汪,這時(shí)候 VO 就變成了 AO
因此執(zhí)行上下文創(chuàng)建的具體過程如下:
- 找到當(dāng)前上下文調(diào)用函數(shù)的代碼
- 執(zhí)行代碼之前脐往,先創(chuàng)建執(zhí)行上下文
- 創(chuàng)建階段:
- 創(chuàng)建變量對(duì)象:
- 創(chuàng)建 arguments 對(duì)象,和參數(shù)
- 掃描上下文的函數(shù)申明:
- 每掃描到一個(gè)函數(shù)什么就會(huì)用函數(shù)名創(chuàng)建一個(gè)屬性扳埂,它是一個(gè)指針业簿,指向該函數(shù)在內(nèi)存中的地址
- 如果函數(shù)名已經(jīng)存在,對(duì)應(yīng)的屬性值會(huì)被新的指針覆蓋
- 掃描上下文的變量申明:
- 每掃描到一個(gè)變量就會(huì)用變量名作為屬性名阳懂,其值初始化為 undefined
- 如果該變量名在變量對(duì)象中已經(jīng)存在梅尤,則直接跳過繼續(xù)掃描
- 初始化作用域鏈
- 確定上下文中 this 的指向
- 創(chuàng)建變量對(duì)象:
- 代碼執(zhí)行階段
- 執(zhí)行函數(shù)體中的代碼柜思,給變量賦值
注意:
- 全局上下文的變量對(duì)象初始化是全局對(duì)象
- 全局上下文的生命周期,與程序的生命周期一致巷燥,只要程序運(yùn)行不結(jié)束赡盘,比如關(guān)掉瀏覽器窗口,全局上下文就會(huì)一直存在缰揪。
- 作用域鏈(scopeChain) 和 this 的指向我們后面再詳細(xì)了解
我們看一個(gè)例子
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
在調(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: { ... }
}
激活階段如下
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)建需要注意以下幾點(diǎn)
- 創(chuàng)建階段的創(chuàng)建順序是:函數(shù)的形參聲明并賦值 ==>> 函數(shù)聲明 ==>> 變量聲明
- 創(chuàng)建階段處理函數(shù)重名和變量重名的策略不同,簡(jiǎn)單來說就是函數(shù)優(yōu)先級(jí)高邀跃。
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(){}
變量提升
通過上面的介紹我們其實(shí)就知道了變量提升這一現(xiàn)象的出現(xiàn)的根本原因就是執(zhí)行上下文在創(chuàng)建的時(shí)候就會(huì)掃描上下文中的變量將其聲明出來霉咨,并設(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)建在變量對(duì)象中僵驰。因此可以訪問它喷斋。
- foo 被聲明了兩次, 為什么 foo 展現(xiàn)出來的是 functiton,而不是undefined 或者 string
在創(chuàng)建階段蒜茴,函數(shù)聲明是優(yōu)先于變量被創(chuàng)建的星爪。而且在變量的創(chuàng)建過程中,如果發(fā)現(xiàn) VO 中已經(jīng)存在相同名稱的屬性粉私,則不會(huì)影響已經(jīng)存在的屬性顽腾。
因此,對(duì) foo() 函數(shù)的引用首先被創(chuàng)建在活動(dòng)對(duì)象里诺核,并且當(dāng)我們解釋到 var foo
時(shí)抄肖,我們看見 foo 屬性名已經(jīng)存在,所以代碼什么都不做并繼續(xù)執(zhí)行窖杀。
- 為什么 bar 的值是 undefined漓摩?
bar 采用的是函數(shù)表達(dá)式的方式來定義的,所以 bar 實(shí)際上是一個(gè)變量入客,但變量的值是函數(shù)管毙,并且我們知道變量在創(chuàng)建階段被創(chuàng)建但他們被初始化為 undefined。