如果你要成为一名出色的前端开发,第一关必须看懂代码。看懂代码的第一步就是掌握执行上下文。它能帮助你理解JS的其他概念。例如变量提升、作用域链、闭包等。
什么是执行上下文
执行上下文是一个为即将执行的代码块做好一切准备而创建的环境。此环境包含了代码运行时所需要的变量、函数、参数以及作用域链和this指向等信息。
执行上下文的类型
目前JS主要的执行上下文的类型分为四种:
-
全局执行上下文 它默认的、最外层的上下文。浏览器环境为
window
Node.js环境global
。可以统一使用globalThis
。 -
函数执行上下文 每次调用函数时,都会为该函数创建一个全新的执行上下文。函数可以访问外部执行上下文的信息。但外部的不能访问函数内部的信息。
-
Eval函数执行上下文 Eval内部的代码会获取自己的上下文。由于Eval的使用带来了安全、性能等问题。强烈推荐不使用。因此在此不讨论。
-
模块执行上下文 模块执行上下文是ES6为模块系统新增的一种执行上下文。模块中声明的顶级变量归模块自身所有。不会成为全局环境的属性。自动地启用严格模式。通过
<script type="module">
和.mjs
方式创建。
执行上下文生成过程
执行上下文生成过程主要分为两个阶段:创建阶段
和执行阶段
创建阶段
在此阶段,执行上下文刚刚创建,但是代码并没有运行。主要完成如下三件事情:
1.创建变量对象(Variable Object VO)
在函数上下文中 VO也称为活动对象(Activation Object AO)
。创建AO的过程经历以下事情:
- 建立一个空的AO对象,在此对象中添加一个名为
arguments
的类数组对象。arguments存放参数值。 - 将形参属性名添加到arguments中,变量名作为属性添加AO对象中,其值为
undefined
。 - 将实参值赋值到arguments类数组中
- 找到函数中的函数声明,以函数名作为属性、函数体为值,添加到AO中去,如果属性重名,则覆盖之前的属性值。
全局执行上下文创建的变量对象为全局对象(Global Object GO)
。过程与函数类似。只是没有参数arguments
2. 建立作用域链
作用域链是一个当前执行上下文和外层执行上下文组成的一个链表的数据结构。它确保了变量和函数的有序访问。例如当查找一个变量时,会按照作用域从当前执行上下文开始查找。一直向外层执行上下文查找,如果找到立刻停止。如果找到全局上下文也没有。则抛出异常。
3. 确定this的指向
this的值在执行上下文创建时确定。全局上下文this
指向全局对象。函数上下文的this
指向取决函数如何被调用的。
执行阶段
在执行阶段,开始代码逐行执行。
- 变量赋值
- 函数引用:执行到函数时,将创建好的执行上下文压入调用栈的顶部,如果在函数内部有遇到函数的执行,将重复上述创建和执行的过程。
代码示例
通过一两个小案例讲解一下执行上下生成的过程。
示例1
js
function fn(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function () {};
console.log(b);
function d() {}
}
fn(1);
在将要执行fn(1)时,开始进入fn执行上下文的创建阶段
- 创建一个fn的执行上下文AO
text
AO = {
argument: { length: 0 }
/* this和作用域链忽略 */
}
- 将形参和变量声明放入argumnets和AO中
text
AO = {
arguments: { 0: undefined, length: 1 },
b: undefined
}
- 实参赋值到arguments中
text
AO = {
arguments: { 0: 1, length: 1},
b: undefined,
}
- 找到函数内的函数声明,以函数名为属性,函数体为值,添加到AO中去.
text
AO = {
arguments: { 0: [Function a] },
b: undefined,
d: [Function d]
}
到此创建阶段结束。准备开始执行阶段
。
- 将创建的函数执行上下文压入调用栈的顶部
- 开始逐行执行代码
- 执行第一个console。此时在执行上下文中可以看到a是一个函数。因此浏览器打印为
f a() {}
- 执行到var a = 123时,将AO中的参数变量改为了123
text
AO = {
arguments: { 0: 123 },
b: undefined,
d: [Function d]
}
- 执行到第二console。打印123
- 执行到第三个console。也打印123
- 执行到var b = funciton () {},将AO的属性b的值修改
text
AO = {
arguments: { 0: 123 },
b: [Function b],
d: [Function d]
}
- 执行到第四个console。浏览器打印为
f () {}
- 函数执行完毕。将fn的执行上下文推出调用栈
示例2
js
function test() {
console.log(b);
if (a) {
var b = 100;
}
c = 234;
console.log(c);
}
var a;
test();
a = 10;
console.log(c);
全局执行上下文
的创建阶段
,创建一个变量对象GO,并找到变量声明放入GO。代码中 c = 234为隐式声明全局变量,强烈不推荐此写法。
text
GO = {
a: undefined,
c: undefined
}
- 找到函数声明并放入GO
text
GO = {
a: undefined,
c: undefined,
test: [Function test]
}
全局执行上下文
的执行阶段
逐行执行代码。直到test()
,将为test创建一个全新的函数执行上下文。
text
AO = {
arguments: { length: 0 },
[[Scopes]]: GO // 作用域链 这就是为什么可以访问外部变量的原因
}
- 没有参数,跳过,找到变量声明。
text
AO = {
arguments: { length: 0 },
b: undefined,
[[scopes]]: GO
}
- 开始test执行上下文的
执行阶段
,将test执行上下文压入栈顶。逐行执行代码。 - 执行第一个console。AO中b还没有赋值。此打印
undefined
。这就是所谓的变量提升
的原理所在 - 执行到
if(a) { var b = 100 }
时,按照作用链查找规则,在全局对象下有a属性但值为undefined。因此没有进入{}
内对b的赋值。 - 执行c=234,同样按照作用链查找规则,在全局对象下有c属性,因此执行赋值操作
text
GO = {
a: undefined,
c: 234,
test: [Function test]
}
- 执行第二个console,也是按照作用链查找规则,因此打印234
- test执行上下文运行完毕,将test执行上下文推出栈外
- 执行a = 10的赋值操作
text
GO = {
a: 10,
c: 234,
test: [Function test]
}
- 执行第三个console。打印234
小结
本章主要讲解了以下内容:
- 执行上下文的概念
- 执行上下文的有哪些类型
- 执行上下文的生成过程分为两个阶段:创建阶段和执行阶段。每个阶段处理了什么。
- 通过示例讲解执行上下文生成过程以及对代码的影响。 理解执行上下文对于我们对JS代码的运行有着至关重要的作用。但是本章还没有详细讲解作用域和作用域链以及This的指向的知识点。下一章将详细讲解作用域和作用域链、以及于执行上下文的关系。