[TOC]
>http://blog.csdn.net/z742182637/article/category/6047401/2
# 深入理解Javascript之執(zhí)行上下文(Execution Context)
在這篇文章中,將比較深入地闡述下執(zhí)行上下文 - Javascript 中最基礎(chǔ)也是最重要的一個(gè)概念。相信讀完這篇文章后,你就會(huì)明白 javascript 引擎內(nèi)部在執(zhí)行代碼以前到底做了些什么,為什么某些函數(shù)以及變量在沒有被聲明以前就可以被使用,以及它們的最終的值是怎樣被定義的。
## 什么是執(zhí)行上下文(**EC**)
Javascript中代碼的運(yùn)行環(huán)境分為以下三種:
* 全局級(jí)別的代碼 - 這個(gè)是默認(rèn)的代碼運(yùn)行環(huán)境,一旦代碼被載入,引擎最先進(jìn)入的就是這個(gè)環(huán)境。
* 函數(shù)級(jí)別的代碼 - 當(dāng)執(zhí)行一個(gè)函數(shù)時(shí),運(yùn)行函數(shù)體中的代碼。
* `eval`的代碼 - 在`eval`函數(shù)內(nèi)運(yùn)行的代碼。
在網(wǎng)上可以找到很多闡述作用域的資源,為了使該文便于大家理解,我們可以將“執(zhí)行上下文”看做當(dāng)前代碼的運(yùn)行環(huán)境或者作用域。
下面我們來(lái)看一個(gè)示例,其中包括了全局以及函數(shù)級(jí)別的執(zhí)行上下文:

上圖中,一共用4個(gè)執(zhí)行上下文。
* <i style="color:#BE00B3">紫色的代表全局的上下文;</i>
* <i style="color:#22CC01">綠色代表person函數(shù)內(nèi)的上下文;</i>
* <i style="color:#0036FF">藍(lán)色</i>以及<i style="color:#FF9600">橙色</i>代表 person 函數(shù)內(nèi)的另外兩個(gè)函數(shù)的上下文。
注意,不管什么情況下,只存在一個(gè)全局的上下文,該上下文能被任何其它的上下文所訪問到。也就是說(shuō),我們可以在 person 的上下文中訪問到全局上下文中的 sayHello 變量,當(dāng)然在函數(shù) firstName 或者 lastName 中同樣可以訪問到該變量。
## 執(zhí)行上下文堆棧(ECS)
一系列活動(dòng)的執(zhí)行上下文從邏輯上形成一個(gè)棧。**棧底總是全局上下文,棧頂是當(dāng)前(活動(dòng)的)執(zhí)行上下文**。當(dāng)在不同的執(zhí)行上下文間切換(退出的而進(jìn)入新的執(zhí)行上下文)的時(shí)候,棧會(huì)被修改(通過(guò)壓棧或者退棧的形式)。
**壓棧:**全局EC—>局部EC1—>局部EC2—>當(dāng)前EC
**出棧:**全局EC<—局部EC1<—局部EC2<—當(dāng)前EC
我們可以用數(shù)組的形式來(lái)表示環(huán)境棧:
```
ECS=[局部EC,全局EC];
```
每次控制器進(jìn)入一個(gè)函數(shù)(哪怕該函數(shù)被遞歸調(diào)用或者作為構(gòu)造器),都會(huì)發(fā)生壓棧的操作。過(guò)程類似 javascript 數(shù)組的 push 和 pop 操作。
在瀏覽器中,javascript 引擎的工作方式是單線程的。也就是說(shuō),某一時(shí)刻**只會(huì)有一個(gè)事件是被激活處理的**,其它的事件被放入隊(duì)列中,等待被處理。
下面的示例圖描述了這樣的一個(gè)堆棧:

我們已經(jīng)知道,**當(dāng)javascript代碼文件被瀏覽器載入后,默認(rèn)最先進(jìn)入的是一個(gè)全局的執(zhí)行上下文**。
當(dāng)在全局上下文中調(diào)用執(zhí)行一個(gè)函數(shù)時(shí),程序流就進(jìn)入該被調(diào)用函數(shù)內(nèi),此時(shí)引擎就會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文,并且將其壓入到執(zhí)行上下文堆棧的頂部。
**瀏覽器總是執(zhí)行當(dāng)前在堆棧頂部的上下文,一旦執(zhí)行完畢,該上下文就會(huì)從堆棧頂部被彈出,然后,進(jìn)入其下的上下文執(zhí)行代碼。last-in first-out stack (LIFO stack)**
這樣,堆棧中的上下文就會(huì)被依次執(zhí)行并且彈出堆棧,直到回到全局的上下文。請(qǐng)看下面一個(gè)例子:
```js
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
```
上述 `foo` 被聲明后,通過(guò) `()` 運(yùn)算符立即執(zhí)行運(yùn)行了。函數(shù)代碼就是調(diào)用了其自身3次,每次是局部變量 `i` 增加1。每次 `foo` 函數(shù)被自身調(diào)用時(shí),就會(huì)有一個(gè)新的執(zhí)行上下文被創(chuàng)建。每當(dāng)一個(gè)上下文執(zhí)行完畢,該上上下文就被彈出堆棧,回到上一個(gè)上下文,直到再次回到全局上下文。整個(gè)過(guò)程抽象如下圖:

由此可見 ,對(duì)于執(zhí)行上下文這個(gè)抽象的概念,可以歸納為以下幾點(diǎn):
* 單線程
* 同步執(zhí)行
* 唯一的一個(gè)全局上下文
* 函數(shù)的執(zhí)行上下文的個(gè)數(shù)沒有限制
* 每次函數(shù)被調(diào)用創(chuàng)建新的執(zhí)行上下文,包括調(diào)用自己。
## 執(zhí)行上下文的建立過(guò)程
我們現(xiàn)在已經(jīng)知道,**每當(dāng)調(diào)用一個(gè)函數(shù)時(shí),一個(gè)新的執(zhí)行上下文就會(huì)被創(chuàng)建出來(lái)**。
JavaScript 代碼自上而下執(zhí)行,但是在 js 代碼執(zhí)行前,javascript 引擎內(nèi)部會(huì)首先進(jìn)行詞法分析,所以事實(shí)上,js 運(yùn)行要分為**預(yù)編譯的詞法分析**和**實(shí)際執(zhí)行**兩個(gè)階段:
### 預(yù)編譯階段(進(jìn)入上下文階段,會(huì)進(jìn)行一系列的詞法分析,發(fā)生在當(dāng)調(diào)用一個(gè)函數(shù)時(shí),但是在執(zhí)行函數(shù)體內(nèi)的具體代碼以前)

* 建立變量,函數(shù),`arguments` 對(duì)象,參數(shù);
* 建立作用域鏈;
* 確定 this 的值;
#### 創(chuàng)建變量對(duì)象
**創(chuàng)建變量對(duì)象**主要是經(jīng)過(guò)以下過(guò)程,如圖所示:

1. 創(chuàng)建 `arguments` 對(duì)象,檢查當(dāng)前上下文的參數(shù),建立該對(duì)象的屬性與屬性值,僅在函數(shù)環(huán)境(非箭頭函數(shù))中進(jìn)行的,全局環(huán)境沒有此過(guò)程。
2. 檢查當(dāng)前上下文的**函數(shù)聲明**,按照代碼順序查找,將找到的函數(shù)提前聲明,如果當(dāng)前上下文的變量對(duì)象沒有該函數(shù)名屬性,則在該變量對(duì)象以函數(shù)名建立一個(gè)屬性,屬性值則指向該函數(shù)所在**堆內(nèi)存地址引用**,如果存在,則會(huì)被新的引用覆蓋掉。
3. 檢查當(dāng)前上下文的**變量聲明**,愛去哪找代碼順序查找,將找到的變量提前聲明,如果當(dāng)前上下文的變量對(duì)象沒有變量名屬性,則在該變量對(duì)象以變量名建立一個(gè)屬性,屬性值為 `undefined`;如果存在,則忽略該變量聲明。
**函數(shù)聲明提前和變量聲明提升是在創(chuàng)建變量對(duì)象中進(jìn)行的,且函數(shù)聲明優(yōu)先級(jí)高于變量聲明**。
**創(chuàng)建變量對(duì)象發(fā)生在預(yù)編譯階段,還沒有進(jìn)入到執(zhí)行階段,該變量對(duì)象都不能訪問的**,因?yàn)榇藭r(shí)的變量對(duì)象中的變量屬性尚未賦值,值仍為 `undefined`,只有在進(jìn)行執(zhí)行階段,變量中的變量屬性才進(jìn)行賦值后,變量對(duì)象(Variable Object)轉(zhuǎn)為活動(dòng)對(duì)象(Active Object)后,才能進(jìn)行訪問,這個(gè)過(guò)程就是 VO -> AO 過(guò)程。
### 執(zhí)行階段
變量賦值,函數(shù)引用,執(zhí)行其它代碼;
實(shí)際上,可以把執(zhí)行上下文看做一個(gè)對(duì)象,其下包含了以上3個(gè)屬性:
~~~
executionContextObj = {
variableObject: { /* 函數(shù)中的arguments對(duì)象, 參數(shù), 內(nèi)部的變量以及函數(shù)聲明 */ },
scopeChain: { /* variableObject 以及所有父執(zhí)行上下文中的variableObject */ },
this: {}
}
~~~
> 進(jìn)入執(zhí)行上下文時(shí),**VO**(variableObject)的初始化過(guò)程具體如下:
> 函數(shù)的形參(當(dāng)進(jìn)入函數(shù)執(zhí)行上下文時(shí))—— 變量對(duì)象的一個(gè)屬性,其屬性名就是形參的名字,其值就是實(shí)參的值;對(duì)于沒有傳遞的參數(shù),其值為 `undefined`;
> 函數(shù)聲明(FunctionDeclaration, **FD**) —— 變量對(duì)象的一個(gè)屬性,其屬性名和值都是函數(shù)對(duì)象創(chuàng)建出來(lái)的;**如果變量對(duì)象已經(jīng)包含了相同名字的屬性,則替換它的值**;
> 變量聲明(var,VariableDeclaration) —— 變量對(duì)象的一個(gè)屬性,其屬性名即為變量名,其值為 `undefined`;如果變量名和已經(jīng)聲明的函數(shù)名或者函數(shù)的參數(shù)名相同,則不會(huì)影響已經(jīng)存在的屬性。
**注意:該過(guò)程是有先后順序的。**
> 執(zhí)行代碼階段時(shí),VO 中的一些屬性 `undefined` 值將會(huì)確定。
## **建立階段以及代碼執(zhí)行階段的詳細(xì)分析**
確切地說(shuō),執(zhí)行上下文對(duì)象(上述的 executionContextObj )是在函數(shù)被調(diào)用時(shí),但是在函數(shù)體被真正執(zhí)行以前所創(chuàng)建的。函數(shù)被調(diào)用時(shí),就是我上述所描述的兩個(gè)階段中的第一個(gè)階段 - 建立階段。
這個(gè)時(shí)刻,引擎會(huì)檢查函數(shù)中的參數(shù),聲明的變量以及內(nèi)部函數(shù),然后基于這些信息建立執(zhí)行上下文對(duì)象(executionContextObj)。
在這個(gè)階段,variableObject 對(duì)象,作用域鏈,以及 this 所指向的對(duì)象都會(huì)被確定。
### AO 活動(dòng)對(duì)象
在函數(shù)的執(zhí)行上下文中,VO 是不能直接訪問的。它主要扮演被稱作活躍對(duì)象(activation object)(簡(jiǎn)稱:**AO**)的角色。
這句話怎么理解呢,就是當(dāng) EC 環(huán)境為函數(shù)時(shí),我們?cè)L問的是 AO,而不是 VO。
不理解,可以看一下 JavaScript高級(jí)程序設(shè)計(jì)的原話:
~~~
function compare(value1,value2){
if (value1<value2){
return -1;
} else if (value1>value2){
return 1;
} else {
return 0;
}
}
var result = compare(5,10)
~~~
以上代碼定義了`compare()`函數(shù),然后又在全局作用域中調(diào)用了它(定義了變量`result`,賦值`compare(5,10)`)
當(dāng)調(diào)用`compare()`時(shí),會(huì)創(chuàng)建一個(gè)包含`arguments`、`value1`、`value2`的 **活動(dòng)對(duì)象**。
全局執(zhí)行環(huán)境的**變量對(duì)象**(包含`result`和`compare`)。
也就是說(shuō):在全局環(huán)境中,沒有了所謂的**活動(dòng)對(duì)象**(AO)概念,當(dāng)我們理解一個(gè)**函數(shù)的運(yùn)行時(shí)**,我們就需要**變量對(duì)象**(VO)來(lái)幫助我們理解,但此時(shí)我們已經(jīng)不太關(guān)心**活動(dòng)對(duì)象**(AO)了,并不是它不存在了。
```
VO(functionContext) === AO;
```
AO 是在進(jìn)入函數(shù)的執(zhí)行上下文時(shí)創(chuàng)建的,并為該對(duì)象初始化一個(gè)`arguments`屬性,該屬性的值為`arguments`對(duì)象。
```
AO = {
arguments: {
callee:,
length:,
properties-indexes: //函數(shù)傳參參數(shù)值
}
};
```
FD 的形式只能是如下這樣:
```js
function f(){
}
```
當(dāng)函數(shù)被調(diào)用是 executionContextObj 被創(chuàng)建,但在實(shí)際函數(shù)執(zhí)行之前。這是我們上面提到的第一階段,創(chuàng)建階段。在此階段,解釋器掃描傳遞給函數(shù)的參數(shù)或 arguments,本地函數(shù)聲明和本地變量聲明,并創(chuàng)建 executionContextObj 對(duì)象。掃描的結(jié)果將完成變量對(duì)象的創(chuàng)建。
上述第一個(gè)階段的具體過(guò)程如下:
1. 找到當(dāng)前上下文中的調(diào)用函數(shù)的代碼
2. 在執(zhí)行被調(diào)用的函數(shù)體中的代碼以前,開始創(chuàng)建執(zhí)行上下文
3. 進(jìn)入第一個(gè)階段-建立階段:
建立variableObject對(duì)象:
1. 建立arguments對(duì)象,檢查當(dāng)前上下文中的參數(shù),建立該對(duì)象下的屬性以及屬性值
2. 檢查當(dāng)前上下文中的函數(shù)聲明:
每找到一個(gè)函數(shù)聲明,就在variableObject下面用函數(shù)名建立一個(gè)屬性,屬性值就是指向該函數(shù)在內(nèi)存中的地址的一個(gè)引用。
如果上述函數(shù)名已經(jīng)存在于variableObject下,那么對(duì)應(yīng)的屬性值會(huì)被新的引用所覆蓋。
3. 檢查當(dāng)前上下文中的變量聲明:
每找到一個(gè)變量的聲明,就在variableObject下,用變量名建立一個(gè)屬性,屬性值為undefined。
如果該變量名已經(jīng)存在于variableObject屬性中,直接跳過(guò)(防止指向函數(shù)的屬性的值被變量屬性覆蓋為undefined),原屬性值不會(huì)被修改。
初始化作用域鏈
確定上下文中 this 的指向?qū)ο?
4. 代碼執(zhí)行階段:
執(zhí)行函數(shù)體中的代碼,一行一行地運(yùn)行代碼,給`variableObject`中的變量屬性賦值。
下面來(lái)看個(gè)具體的代碼示例:
```js
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
```
在調(diào)用`foo(22)`的時(shí)候,建立階段如下:
```
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c:<pointer to function c()>
a: undefined,
b: undefined
},
scopeChain: { ... },
this: { ... }
}
```
由此可見,在建立階段,除了`arguments`,函數(shù)的聲明,以及參數(shù)被賦予了具體的屬性值,其它的變量屬性默認(rèn)的都是undefined。一旦上述建立階段結(jié)束,引擎就會(huì)進(jìn)入代碼執(zhí)行階段,這個(gè)階段完成后,上述執(zhí)行上下文對(duì)象如下:
```
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: <pointer to function privateB()>
},
scopeChain: { ... },
this: { ... }
}
```
我們看到,**只有在代碼執(zhí)行階段,變量屬性才會(huì)被賦予具體的值**。
## 局部變量作用域提升的緣由
在網(wǎng)上一直看到這樣的總結(jié): 在函數(shù)中聲明的變量以及函數(shù),其作用域提升到函數(shù)頂部,換句話說(shuō),就是一進(jìn)入函數(shù)體,就可以訪問到其中聲明的變量以及函數(shù)。這是對(duì)的,但是知道其中的緣由嗎?相信你通過(guò)上述的解釋應(yīng)該也有所明白了。不過(guò)在這邊再分析一下??聪旅嬉欢未a:
```js
(function() {
console.log(typeof foo); // function
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
console.log(typeof foo); // string
console.log(typeof bar); // function
}());
```
上述代碼定義了一個(gè)匿名函數(shù),并且通過(guò) `()` 運(yùn)算符強(qiáng)制理解執(zhí)行。那么我們知道這個(gè)時(shí)候就會(huì)有個(gè)執(zhí)行上下文被創(chuàng)建,我們看到例子中馬上可以訪問 `foo` 以及 `bar` 變量,并且通過(guò) `typeof` 輸出 `foo` 為一個(gè)函數(shù)引用,`bar` 為 `undefined`。
**為什么我們可以在聲明 foo 變量以前就可以訪問到 foo 呢?**
因?yàn)樵谏舷挛牡慕㈦A段,先是處理 `arguments`, 參數(shù),接著是函數(shù)的聲明,最后是變量的聲明。那么,發(fā)現(xiàn) `foo`函數(shù)的聲明后,就會(huì)在variableObject 下面建立一個(gè) `foo` 屬性,其值是一個(gè)指向函數(shù)的引用。當(dāng)處理變量聲明的時(shí)候,發(fā)現(xiàn)有 `var foo` 的聲明,但是 variableObject已經(jīng)具有了 `foo` 屬性,所以直接跳過(guò)。當(dāng)進(jìn)入代碼執(zhí)行階段的時(shí)候,就可以通過(guò)訪問到 `foo` 屬性了,因?yàn)樗呀?jīng)就存在,并且是一個(gè)函數(shù)引用。
**為什么 `bar` 是 `undefined` 呢?**
因?yàn)閌bar`是變量的聲明,在建立階段的時(shí)候,被賦予的默認(rèn)的值為 `undefined`。由于它只要在代碼執(zhí)行階段才會(huì)被賦予具體的值,所以,當(dāng)調(diào)用 `typeof(bar)` 的時(shí)候輸出的值為 `undefined`。
到此,相信你應(yīng)該對(duì)執(zhí)行上下文有所理解了,這個(gè)執(zhí)行上下文的概念非常重要,務(wù)必好好搞懂之!
# 誰(shuí)先被提升?
再來(lái)個(gè)例子,`foo` 是先提升變量聲明 還是 函數(shù)聲明 ?
```js
console.log(typeof foo); // function
var foo = "this is var foo";
function foo() {
console.log("this is function foo");
}
console.log(typeof foo) // string
```
`foo` 函數(shù)應(yīng)該是先被整體提升(不是 `undefined`),然后才是 變量提升。
# 參考
《測(cè)試驅(qū)動(dòng)的JavaScript開發(fā)》-第五章 函數(shù)
- 步入JavaScript的世界
- 二進(jìn)制運(yùn)算
- JavaScript 的版本是怎么回事?
- JavaScript和DOM的產(chǎn)生與發(fā)展
- DOM事件處理
- js的并行加載與順序執(zhí)行
- 正則表達(dá)式
- 當(dāng)遇上this時(shí)
- Javascript中apply、call、bind
- JavaScript的編譯過(guò)程與運(yùn)行機(jī)制
- 執(zhí)行上下文(Execution Context)
- javascript 作用域
- 分組中的函數(shù)表達(dá)式
- JS之constructor屬性
- Javascript 按位取反運(yùn)算符 (~)
- EvenLoop 事件循環(huán)
- 異步編程
- JavaScript的九個(gè)思維導(dǎo)圖
- JavaScript奇淫技巧
- JavaScript:shim和polyfill
- ===值得關(guān)注的庫(kù)===
- ==文章==
- JavaScript框架
- Angular 1.x
- 啟動(dòng)引導(dǎo)過(guò)程
- $scope作用域
- $q與promise
- ngRoute 和 ui-router
- 雙向數(shù)據(jù)綁定
- 規(guī)范和性能優(yōu)化
- 自定義指令
- Angular 事件
- lodash
- Test
