[TOC]
>http://blog.csdn.net/z742182637/article/category/6047401/2
# 深入理解Javascript之执行上下文(Execution Context)
在这篇文章中,将比较深入地阐述下执行上下文 - Javascript 中最基础也是最重要的一个概念。相信读完这篇文章后,你就会明白 javascript 引擎内部在执行代码以前到底做了些什么,为什么某些函数以及变量在没有被声明以前就可以被使用,以及它们的最终的值是怎样被定义的。
## 什么是执行上下文(**EC**)
Javascript中代码的运行环境分为以下三种:
* 全局级别的代码 - 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
* 函数级别的代码 - 当执行一个函数时,运行函数体中的代码。
* `eval`的代码 - 在`eval`函数内运行的代码。
在网上可以找到很多阐述作用域的资源,为了使该文便于大家理解,我们可以将“执行上下文”看做当前代码的运行环境或者作用域。
下面我们来看一个示例,其中包括了全局以及函数级别的执行上下文:
![](https://box.kancloud.cn/b6addd1c134b1034fdacb797db2e651c_554x447.png)
上图中,一共用4个执行上下文。
* <i style="color:#BE00B3">紫色的代表全局的上下文;</i>
* <i style="color:#22CC01">绿色代表person函数内的上下文;</i>
* <i style="color:#0036FF">蓝色</i>以及<i style="color:#FF9600">橙色</i>代表 person 函数内的另外两个函数的上下文。
注意,不管什么情况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也就是说,我们可以在 person 的上下文中访问到全局上下文中的 sayHello 变量,当然在函数 firstName 或者 lastName 中同样可以访问到该变量。
## 执行上下文堆栈(ECS)
一系列活动的执行上下文从逻辑上形成一个栈。**栈底总是全局上下文,栈顶是当前(活动的)执行上下文**。当在不同的执行上下文间切换(退出的而进入新的执行上下文)的时候,栈会被修改(通过压栈或者退栈的形式)。
**压栈:**全局EC—>局部EC1—>局部EC2—>当前EC
**出栈:**全局EC<—局部EC1<—局部EC2<—当前EC
我们可以用数组的形式来表示环境栈:
```
ECS=[局部EC,全局EC];
```
每次控制器进入一个函数(哪怕该函数被递归调用或者作为构造器),都会发生压栈的操作。过程类似 javascript 数组的 push 和 pop 操作。
在浏览器中,javascript 引擎的工作方式是单线程的。也就是说,某一时刻**只会有一个事件是被激活处理的**,其它的事件被放入队列中,等待被处理。
下面的示例图描述了这样的一个堆栈:
![](https://box.kancloud.cn/df4e634fd5133eb8ae456e6ad5bd880c_555x288.png)
我们已经知道,**当javascript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文**。
当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。
**浏览器总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然后,进入其下的上下文执行代码。last-in first-out stack (LIFO stack)**
这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。请看下面一个例子:
```js
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
```
上述 `foo` 被声明后,通过 `()` 运算符立即执行运行了。函数代码就是调用了其自身3次,每次是局部变量 `i` 增加1。每次 `foo` 函数被自身调用时,就会有一个新的执行上下文被创建。每当一个上下文执行完毕,该上上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。整个过程抽象如下图:
![](https://box.kancloud.cn/b41621c52e822cb1754d99ab14e6fcfa_390x259.png)
由此可见 ,对于执行上下文这个抽象的概念,可以归纳为以下几点:
* 单线程
* 同步执行
* 唯一的一个全局上下文
* 函数的执行上下文的个数没有限制
* 每次函数被调用创建新的执行上下文,包括调用自己。
## 执行上下文的建立过程
我们现在已经知道,**每当调用一个函数时,一个新的执行上下文就会被创建出来**。
JavaScript 代码自上而下执行,但是在 js 代码执行前,javascript 引擎内部会首先进行词法分析,所以事实上,js 运行要分为**预编译的词法分析**和**实际执行**两个阶段:
### 预编译阶段(进入上下文阶段,会进行一系列的词法分析,发生在当调用一个函数时,但是在执行函数体内的具体代码以前)
![](https://box.kancloud.cn/5c6be302e9d98ca677c4d93df4245f65_387x315.png)
* 建立变量,函数,`arguments` 对象,参数;
* 建立作用域链;
* 确定 this 的值;
#### 创建变量对象
**创建变量对象**主要是经过以下过程,如图所示:
![](https://box.kancloud.cn/33c89d0faf54f4076bca04eae4e284ca_590x232.png)
1. 创建 `arguments` 对象,检查当前上下文的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行的,全局环境没有此过程。
2. 检查当前上下文的**函数声明**,按照代码顺序查找,将找到的函数提前声明,如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则指向该函数所在**堆内存地址引用**,如果存在,则会被新的引用覆盖掉。
3. 检查当前上下文的**变量声明**,爱去哪找代码顺序查找,将找到的变量提前声明,如果当前上下文的变量对象没有变量名属性,则在该变量对象以变量名建立一个属性,属性值为 `undefined`;如果存在,则忽略该变量声明。
**函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明**。
**创建变量对象发生在预编译阶段,还没有进入到执行阶段,该变量对象都不能访问的**,因为此时的变量对象中的变量属性尚未赋值,值仍为 `undefined`,只有在进行执行阶段,变量中的变量属性才进行赋值后,变量对象(Variable Object)转为活动对象(Active Object)后,才能进行访问,这个过程就是 VO -> AO 过程。
### 执行阶段
变量赋值,函数引用,执行其它代码;
实际上,可以把执行上下文看做一个对象,其下包含了以上3个属性:
~~~
executionContextObj = {
variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ },
scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
this: {}
}
~~~
> 进入执行上下文时,**VO**(variableObject)的初始化过程具体如下:
> 函数的形参(当进入函数执行上下文时)—— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为 `undefined`;
> 函数声明(FunctionDeclaration, **FD**) —— 变量对象的一个属性,其属性名和值都是函数对象创建出来的;**如果变量对象已经包含了相同名字的属性,则替换它的值**;
> 变量声明(var,VariableDeclaration) —— 变量对象的一个属性,其属性名即为变量名,其值为 `undefined`;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
**注意:该过程是有先后顺序的。**
> 执行代码阶段时,VO 中的一些属性 `undefined` 值将会确定。
## **建立阶段以及代码执行阶段的详细分析**
确切地说,执行上下文对象(上述的 executionContextObj )是在函数被调用时,但是在函数体被真正执行以前所创建的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 - 建立阶段。
这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,然后基于这些信息建立执行上下文对象(executionContextObj)。
在这个阶段,variableObject 对象,作用域链,以及 this 所指向的对象都会被确定。
### AO 活动对象
在函数的执行上下文中,VO 是不能直接访问的。它主要扮演被称作活跃对象(activation object)(简称:**AO**)的角色。
这句话怎么理解呢,就是当 EC 环境为函数时,我们访问的是 AO,而不是 VO。
不理解,可以看一下 JavaScript高级程序设计的原话:
~~~
function compare(value1,value2){
if (value1<value2){
return -1;
} else if (value1>value2){
return 1;
} else {
return 0;
}
}
var result = compare(5,10)
~~~
以上代码定义了`compare()`函数,然后又在全局作用域中调用了它(定义了变量`result`,赋值`compare(5,10)`)
当调用`compare()`时,会创建一个包含`arguments`、`value1`、`value2`的 **活动对象**。
全局执行环境的**变量对象**(包含`result`和`compare`)。
也就是说:在全局环境中,没有了所谓的**活动对象**(AO)概念,当我们理解一个**函数的运行时**,我们就需要**变量对象**(VO)来帮助我们理解,但此时我们已经不太关心**活动对象**(AO)了,并不是它不存在了。
```
VO(functionContext) === AO;
```
AO 是在进入函数的执行上下文时创建的,并为该对象初始化一个`arguments`属性,该属性的值为`arguments`对象。
```
AO = {
arguments: {
callee:,
length:,
properties-indexes: //函数传参参数值
}
};
```
FD 的形式只能是如下这样:
```js
function f(){
}
```
当函数被调用是 executionContextObj 被创建,但在实际函数执行之前。这是我们上面提到的第一阶段,创建阶段。在此阶段,解释器扫描传递给函数的参数或 arguments,本地函数声明和本地变量声明,并创建 executionContextObj 对象。扫描的结果将完成变量对象的创建。
上述第一个阶段的具体过程如下:
1. 找到当前上下文中的调用函数的代码
2. 在执行被调用的函数体中的代码以前,开始创建执行上下文
3. 进入第一个阶段-建立阶段:
建立variableObject对象:
1. 建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值
2. 检查当前上下文中的函数声明:
每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用。
如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。
3. 检查当前上下文中的变量声明:
每找到一个变量的声明,就在variableObject下,用变量名建立一个属性,属性值为undefined。
如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。
初始化作用域链
确定上下文中 this 的指向对象
4. 代码执行阶段:
执行函数体中的代码,一行一行地运行代码,给`variableObject`中的变量属性赋值。
下面来看个具体的代码示例:
```js
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
```
在调用`foo(22)`的时候,建立阶段如下:
```
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c:<pointer to function c()>
a: undefined,
b: undefined
},
scopeChain: { ... },
this: { ... }
}
```
由此可见,在建立阶段,除了`arguments`,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下:
```
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: <pointer to function privateB()>
},
scopeChain: { ... },
this: { ... }
}
```
我们看到,**只有在代码执行阶段,变量属性才会被赋予具体的值**。
## 局部变量作用域提升的缘由
在网上一直看到这样的总结: 在函数中声明的变量以及函数,其作用域提升到函数顶部,换句话说,就是一进入函数体,就可以访问到其中声明的变量以及函数。这是对的,但是知道其中的缘由吗?相信你通过上述的解释应该也有所明白了。不过在这边再分析一下。看下面一段代码:
```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
}());
```
上述代码定义了一个匿名函数,并且通过 `()` 运算符强制理解执行。那么我们知道这个时候就会有个执行上下文被创建,我们看到例子中马上可以访问 `foo` 以及 `bar` 变量,并且通过 `typeof` 输出 `foo` 为一个函数引用,`bar` 为 `undefined`。
**为什么我们可以在声明 foo 变量以前就可以访问到 foo 呢?**
因为在上下文的建立阶段,先是处理 `arguments`, 参数,接着是函数的声明,最后是变量的声明。那么,发现 `foo`函数的声明后,就会在variableObject 下面建立一个 `foo` 属性,其值是一个指向函数的引用。当处理变量声明的时候,发现有 `var foo` 的声明,但是 variableObject已经具有了 `foo` 属性,所以直接跳过。当进入代码执行阶段的时候,就可以通过访问到 `foo` 属性了,因为它已经就存在,并且是一个函数引用。
**为什么 `bar` 是 `undefined` 呢?**
因为`bar`是变量的声明,在建立阶段的时候,被赋予的默认的值为 `undefined`。由于它只要在代码执行阶段才会被赋予具体的值,所以,当调用 `typeof(bar)` 的时候输出的值为 `undefined`。
到此,相信你应该对执行上下文有所理解了,这个执行上下文的概念非常重要,务必好好搞懂之!
# 谁先被提升?
再来个例子,`foo` 是先提升变量声明 还是 函数声明 ?
```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` 函数应该是先被整体提升(不是 `undefined`),然后才是 变量提升。
# 参考
《测试驱动的JavaScript开发》-第五章 函数
- 步入JavaScript的世界
- 二进制运算
- JavaScript 的版本是怎么回事?
- JavaScript和DOM的产生与发展
- DOM事件处理
- js的并行加载与顺序执行
- 正则表达式
- 当遇上this时
- Javascript中apply、call、bind
- JavaScript的编译过程与运行机制
- 执行上下文(Execution Context)
- javascript 作用域
- 分组中的函数表达式
- JS之constructor属性
- Javascript 按位取反运算符 (~)
- EvenLoop 事件循环
- 异步编程
- JavaScript的九个思维导图
- JavaScript奇淫技巧
- JavaScript:shim和polyfill
- ===值得关注的库===
- ==文章==
- JavaScript框架
- Angular 1.x
- 启动引导过程
- $scope作用域
- $q与promise
- ngRoute 和 ui-router
- 双向数据绑定
- 规范和性能优化
- 自定义指令
- Angular 事件
- lodash
- Test