0%

执行上下文

执行上下文是什么?

可以理解为当前代码的运行环境,在 js 中,运行环境主要包含了全局环境和函数环境,一段代码一般都有多个执行时上下文。

如何管理多个执行上下文?

执行上下文是以栈(一种 LIFO 的数据结构)的方式被存放起来的,我们称之为执行上下文栈(Execution Context Stack)

代码执行过程中执行上下文栈的行为

  1. 全局上下文入栈:在 JavaScript 代码开始执行时,首先进入全局环境,此时全局上下文被创建并入栈,
  2. 函数上下文入栈:之后当调用函数时则进入相应的函数环境,此时相应函数上下文被创建并入栈
  3. 依次出栈:当处于栈顶的执行上下文代码执行完毕后,则会将其出栈。

所以在执行上下文栈中,栈底永远是全局上下文,而栈顶则是当前正在执行的函数上下文

举个🌰

1
2
3
4
5
6
7
8
function fn2() {
console.log('fn2')
}
function fn1() {
console.log('fn1')
fn2();
}
fn1();

上述代码在执行过程中执行上下文栈的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 伪代码 以数组来表示执行上下文栈 ECStack=[] */
// 代码执行时最先进入全局环境,全局上下文被创建并入栈
ECStack.push(global_EC);
// fn1 被调用,fn1 函数上下文被创建并入栈
ECStack.push(fn1_EC);
// fn1 中调用 fn2,fn2 函数上下文被创建并入栈
ECStack.push(fn2_EC);
// fn2 执行完毕,fn2 函数上下文出栈
ECStack.pop();
// fn1 执行完毕,fn1 函数上下文出栈
ECStack.pop();
// 代码执行完毕,全局上下文出栈
ECStack.pop();

可以自己试试画个图来加深理解

前面理解了执行上下文栈的这种方式管理多个执行上下文,并且举个例子解释了一段代码执行过程中,执行上下文栈对执行上下文的行为。都是大于执行上下文的内容,接下来我们来理解单个执行上下文。

单个执行上下文

在一个执行上下文中,最重要的三个属性分别是变量对象(Variable Object)、作用域链(Scope Chain)和 this 指向。

我们可以采用如下方式表示

1
2
3
4
5
EC = {
VO,
SC,
this
}

一个执行上下文的生命周期分为创建和执行阶段。创建阶段主要工作是生成变量对象、建立作用域链和确定 this 指向。而执行阶段主要工作是变量赋值以及执行其它代码等。

变量对象

在执行上下文的创建阶段会生成变量对象,生成变量对象主要有以下三个过程:

  1. 检索当前上下文中的参数。该过程生成 Arguments 对象,并建立以形参变量名为属性名,形参变量值为属性值的属性;
  2. 检索当前上下文中的函数声明。该过程建立以函数名为属性名,函数所在内存地址引用为属性值的属性;
  3. 检索当前上下文中的变量声明。该过程建立以变量名为属性名,undefined 为属性值的属性(如果变量名跟已声明的形参变量名或函数名相同,则该变量声明不会干扰已经存在的这类属性)。

伪代码来表示变量对象

1
2
3
4
5
6
AO = {
Arguments: {},
ParamVariable: 具体值, //形参变量
Function: <function reference>,
Variable:具体值
}

这里相当于占个位置,就像变量先声明,后赋值

在执行上下文执行阶段,变量对象会变为活动对象(Active Object)。此时原先声明的变量会被赋值

变量对象和活动对象都是指同一个对象,只是处于执行上下文的不同阶段

1
2
3
4
5
6
AO = {
Arguments: {},
ParamVariable: 具体值, //形参变量
Function: <function reference>,
Variable:具体值
}

同样举个🌰

1
2
3
4
5
6
function fn1(a) {
var b = 1;
function fn2() {}
var c = function () {};
}
fn1(0);

当fn1函数被调用时,执行上下文被创建并入栈,其变量对象如下:

1
2
3
4
5
6
7
8
9
10
11
12
fn1_EC = {
VO = {
Arguments: {
'0': 0,
length: 1,
},
a: 0,
b: undefined,
fn2: <function fn2 reference>,
c: undefined,
}
}

当fn1函数在执行过程中时,变量对象变为活动对象,原来生命的变量会被赋值,活动对象如下:

1
2
3
4
5
6
7
8
9
10
11
12
fn1_EC = {
AO = {
Arguments: {
'0': 0,
length: 1,
},
a: 0,
b: 1,
fn2: <function fn2 reference>,
c: <function express c reference>,
}
}
变量对象的应用

在浏览器环境中,全局上下文中的变量对象(全局对象)即我们熟悉的 window 对象,通过该对象可以使用其预定义的变量和函数,在全局环境中所声明的变量和函数,也会成为全局对象的属性。

通过变量对象的生成过程,我们就可以理解函数提升和变量提升的机制了

1
2
3
4
5
6
console.log(a) // undefined
fn(0); // fn
var a = 0;
function fn() {
console.log('fn')
}

理解这段代码的变量提升和函数提升,根据执行上下文的创建和执行过程中,变量对象变为活动对象的过程

上述代码中,在全局上下文的创建阶段,会检索上下文中的函数声明以及变量声明,函数会被赋值具体的引用地址而变量会被赋值为 undefined。

所以其运行结果其实如下:

1
2
3
4
5
6
7
function fn() {
console.log('fn')
}
var a = undefined;
console.log(a) // undefined
fn(0); // fn
a = 0;
作用域链(Scope Chain)

作用域链是指由当前上下文和上层上下文的一系列变量对象组成的层级链。它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

执行上下文分为创建和执行两个阶段,在执行上下文的执行阶段,当需要查找某个变量或函数时,会在当前上下文的变量对象(活动对象)中进行查找,若是没有找到,则会沿着上层上下文的变量对象进行查找,直到全局上下文中的变量对象(全局对象)

如何查找:

依靠当前上下文中的作用域链,其包含了当前上下文和上层上下文中的变量对象,以便其一层一层地去查找其所需要的变量和函数

作用域链怎么建立

JavaScript 中主要包含了全局作用域和函数作用域,函数作用域是在函数被声明的时候确定的

每一个函数都会包含一个 [[scope]] 内部属性,在函数被声明的时候,该函数的 [[scope]] 属性会保存其上层上下文的变量对象,形成包含上层上下文变量对象的层级链。[[scope]] 属性的值是在函数被声明的时候确定的。

当函数被调用的时候,其执行上下文会被创建并入栈。在创建阶段生成其变量对象后,会将该变量对象添加到作用域链的顶端并将 [[scope]] 添加进该作用域链中。而在执行阶段,变量对象会变为活动对象,其相应属性会被赋值。

所以,作用域链是由当前上下文变量对象及上层上下文变量对象组成的

1
SC = AO + [[scope]]

举个🌰

1
2
3
4
5
6
7
8
9
var a = 1;
function fn1() {
var b = 1;
function fn2() {
var c = 1;
}
fn2();
}
fn1();

在 fn1 函数上下文中,fn2 函数被声明,所以

1
fn2.[[scope]] = [fn1_EC.VO, globalObj]

当 fn2 被调用的时候,其执行上下文被创建并入栈,此时会将生成的变量对象添加进作用域链的顶端,并且将 [[scope]] 添加进作用域链

1
2
3
fn2_EC.SC = [fn2_EC.VO].contact(fn2.[[scope]])
==>
fn2_EC.SC = [fn2_EC.VO, fn1_EC.VO, globalObj]

this 指向

this 的指向,是在函数被调用的时候确定的, 也就是执行上下文被创建时确定的

关于 this 的指向,其实最主要的是三种场景,分别是全局上下文中 this、函数中 this 和构造函数中 this

全局上下文中 this

在全局上下文中,this 指代全局对象

1
2
3
4
5
6
7
// 在浏览器环境中,全局对象是 window 对象:
console.log(this === window); // true
a = 1;
this.b = 2;
console.log(window.a); // 1
console.log(window.b); // 2
console.log(b); // 2
函数中 this

如果被调用的函数,被某一个对象所拥有,那么其内部的 this 指向该对象;如果该函数被独立调用,那么其内部的 this 指向 undefined(非严格模式下指向 window)。

1
2
3
4
5
6
7
8
9
10
var a = 1;
function fn() {
console.log(this.a)
}
var obj = {
a: 2,
fn: fn
}
obj.fn(); // 2
fn(); // 1
构造函数中 this

要清楚构造函数中 this 的指向,则必须先了解通过 new 操作符调用构造函数时所经历的阶段。

通过 new 操作符调用构造函数时所经历的阶段如下:

创建一个新对象;
将构造函数的 this 指向这个新对象;
执行构造函数内部代码;
返回这个新对象。

1
2
3
4
5
6
7
function Person(name, age) {
this.name = name;
this.age = age;
}
var ttsy = new Person('ttsy', 24);
console.log(ttsy.name); // ttsy
console.log(ttsy.age); // 24

需要注意的是,在 ES6 中箭头函数中,this 是在函数声明的时候确定的,具体可看 http://es6.ruanyifeng.com/#docs/function

一个完整的🌰

1
2
3
4
5
6
7
8
function fn1() {
var a = 1;
function fn2(b) {
var c = 3
}
fn2(2)
}
fn1();

代码执行过程中,执行上下文栈的变化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 伪代码 以数组来表示执行上下文栈 ECStack=[] */
// 代码执行时最先进入全局环境,全局上下文被创建并入栈
ECStack.push(global_EC);
// fn1 被调用,fn1 函数上下文被创建并入栈
ECStack.push(fn1_EC);
// fn1 中调用 fn2,fn2 函数上下文被创建并入栈
ECStack.push(fn2_EC);
// fn2 执行完毕,fn2 函数上下文出栈
ECStack.pop();
// fn1 执行完毕,fn1 函数上下文出栈
ECStack.pop();
// 代码执行完毕,全局上下文出栈
ECStack.pop();

首先进入全局环境,全局上下文被创建并入栈

1
2
3
4
5
global_EC = {
VO: globalObj,
SC: [globalObj],
this: globalObj,
}

fn1被调用,fn1函数上下文被创建并入栈

fn1函数被调用之前,会有一个函数声明,发生在全局上下文被创建的过程,fn1.[[scope]]会保持上层作用域的变量对象

在 fn1 函数上下文创建阶段,其执行上下文如下

1
2
3
4
5
6
7
8
9
10
11
fn1_EC = {
VO: {
Arguments: {
length: 0
},
fn2: <function fn2 reference>,
a: undefined,
},
SC: [fn1_EC.VO, globalObj],
this: null,
}

在 fn1 函数上下文执行阶段,其执行上下文如下

1
2
3
4
5
6
7
8
9
10
11
fn1_EC = {
VO: {
Arguments: {
length: 0
},
fn2: <function fn2 reference>,
a: 1,
},
SC: [fn1_EC.VO, globalObj],
this: global,
}

然后在 fn1 中调用 fn2,fn2 函数上下文被创建并入栈
在 fn2 函数上下文创建阶段,其执行上下文如下

1
2
3
4
5
6
7
8
9
10
fn2_EC = {
VO: {
Arguments: {
length: 0
},
c: undefined,
},
SC: [fn2_EC.VO, fn1_EC.VO, globalObj],
this: null,
}

在 fn2 函数上下文执行阶段,其执行上下文如下

1
2
3
4
5
6
7
8
9
10
fn2_EC = {
VO: {
Arguments: {
length: 0
},
c: 3,
},
SC: [fn2_EC.VO, fn1_EC.VO, globalObj],
this: global,
}

最后是各个上下文出栈
在各个上下文出栈后,其对应的变量对象会被 JavaScript 中的自动垃圾收集机制回收。

而我们经常说闭包能够访问其所在环境的变量,其实是因为闭包能够阻止上述变量对象被回收的过程