我们在学习作用域或者闭包时,总是绕不开执行上下文,执行栈等术语,那到底什么是执行上下文呢?
一、什么是执行上下文
执行上下文(Execution Context),简称EC。
网上有很多关于执行上下文定义的描述,简单理解一下,其实就是作用域,也就是运行这段JavaScript代码的一个环境。
二、执行上下文的组成和分类
1. 组成
对于每个执行上下文EC,都有三个重要的属性:
- 变量对象Variable Object(变量声明、函数声明、函数形参)
- 作用域链 Scope Chain
- this指针
2. 分类
执行上下文分为3类
- 全局执行上下文
- 函数执行上下文
- eval执行上下文(几乎不用,暂时不做解释)
【全局执行上下文】
术语理解
代码开始执行前首先进入的环境。
特点
全局执行上下文有且只有一个。客户端中一般由浏览器创建,也就是
window对象。
注意点
(1)使用
var声明的全局变量,都可以在window对象中访问到,可以理解为window是var声明对象的载体。(2)使用
let声明的全局变量,用window对象访问不到。
【函数执行上下文】
术语理解
函数被调用时,会创建一个函数执行上下文。
特点
函数执行上下文可以有多个,即使调用自身,也会创建一个新的函数执行上下午呢。
以上是对全局执行上下文和函数执行上下文的区别。
下面再来看看执行上下文的生命周期。
三、执行上下文的生命周期
执行上下文的生命周期可以分为3个阶段:
- 创建阶段
- 执行阶段
- 回收阶段
1. 创建阶段
发生在当函数被调用,但是在未执行内部代码之前。
创建阶段主要做的事情是:
(1)创建变量对象
Variable Object(创建函数形参、函数声明、变量声明)
(2)创建作用域链Scope Chain
(3)确定this指向This Binding
我们先用代码来更直观的理解下创建阶段的过程:
function foo(i){
var a = 100;
var b = function(){};
function c(){}
}
foo(20);
复制代码
当调用foo(20)的时候,执行上下文的创建状态如下:
ExecutionContext:{
scopeChain:{ ... },
this:{ ... },
variableObject:{
arguments:{
0: 20,
length: 1
},
i: 20,
c:<function>,
a:undefined,
b:undefined
}
}
复制代码
2. 执行阶段
创建完成后,程序自动进入执行阶段,执行阶段主要做的事情是:
(1)给变量对象赋值:给
VO中的变量赋值,给函数表达式赋值。
(2)调用函数
(3)顺序执行代码
还是以上面的代码为例,执行阶段给VO赋值,用伪代码表示如下:
ExecutionContext:{
scopeChain:{ ... },
this:{ ... },
variableObject:{
arguments:{
0: 20,
length: 1
},
i: 20,
c:<function>,
a:100,
b:function
}
}
复制代码
3. 回收阶段
所有代码执行完毕,程序关闭,释放内存。
上下文出栈后,虚拟机进行回收。
全局上下文只有当关闭浏览器时才会出栈。
根据以上内容,我们了解到执行上下文的创建需要创建变量对象,那变量对象到底是什么呢?
四、变量对象 VO 和 活动对象 AO
1. VO 概念理解
变量对象
Variable Object,简称VO。简单理解就是一个对象,这个对象存放的是:全局执行上下文的变量和函数。
VO === this === Global
VO的两种特殊情况:
(1)未经过
var声明的变量,不会存在VO中
(2)函数表达式(与函数声明相对),也不在VO中
2. AO 概念理解
活动对象
Activation Object,也叫激活对象,简称AO。
激活对象是在进入函数执行上下文时(函数执行的前一刻)被创建的。
函数执行上下文中,VO是不能直接访问,所以AO扮演了VO的角色。
VO === AO,并且添加了形参类数组和形参的值
Arguments Object是函数上下文AO的一个对象,它包含的属性有:
(1)callee:指向当前函数的引用
(2)length:真正传递参数的个数
(3)properties-indexes:函数的参数值(按照参数列表从左到右排列)
3. VO 的初始化过程
(1)根据函数参数,创建并初始化arguments
变量声明
var、函数形参、函数声明
(2)扫描函数声明
函数声明,是变量对象的一个属性,其属性名和值都是函数对象创建出来的。若变量对象已经包含了相同名字的属性,则替换它的值。
(3)扫描变量声明
变量声明,即变量对象的一个属性,其属性名即变量名,其值为
undefined。如果变量名和已经声明的函数名或者函数的参数名相同,则不影响已经存在的属性。
注:函数声明优先级高于变量声明优先级
五、示例分析
1. 如何理解函数声明中“若变量对象已经包含了相同名字的属性,则替换它的值”
用代码来理解一下:
function fun(a){
console.log(a); // function a(){}
function a(){}
}
fun(100);
复制代码
我们调用了fun(100),传入a的值是100,为什么执行console语句后结果却不是100呢?别急,我们接着分析~
创建阶段:
步骤 1-1:根据形参创建arguments,用实参赋值给对应的形参,没有实参的赋值为undefined
AO_Step1:{
arguments:{
0: 100,
length:1
},
a: 100
}
步骤 1-2:扫描函数声明,此时发现名称为a的函数声明,将其添加到AO上,替换掉已经存在的相同属性名称a,也就是替换掉形参为a的值。
AO_Step2:{
arguments:{
0: 100,
length:1
},
a: 指向function a(){}
}
步骤 1-3:扫描变量声明,未发现有变量。
复制代码
执行阶段:
步骤 2-1:没有赋值语句,第一行执行console命令,而此时a指向的是funciton,所以输出function a(){}
复制代码
2. 如何理解变量声明中“如果变量名和已经声明的函数名或者函数的参数名相同,则不影响已经存在的属性”
用代码来理解一下
情景1:变量与参数名相同
function fun2(a){
console.log(a); // 100
var a = 10;
console.log(a) // 10
}
fun2(100);
// 分析步骤:
创建阶段:
步骤 1-1:根据arguments创建并初始化AO
AO = {
arguments:{
0: 100,
length:1
},
a:100
}
步骤 1-2:扫描函数声明,此时没有额外的函数声明,所以AO还是和上次一致
AO = {
arguments:{
0: 100,
length:1
},
a:100
}
步骤 1-3:扫描变量声明,发现AO中已经存在了a属性,所以不修改已存在的属性。
AO = {
arguments:{
0: 100,
length:1
},
a:100
}
执行阶段:
步骤 2-1:按顺序执行console语句,此时AO中的a是100,所以输出100.
步骤 2-2:执行到赋值语句,对AO中的a进行赋值,此时a是10。
步骤 2-3:按顺序执行,执行console语句,此时a是10,所以输出10。
复制代码
情景2:变量与函数名相同
function fun3(){
console.log(a); // function a(){}
var a = 10;
function a(){}
console.log(a) // 10
}
fun3();
// 分析步骤:
创建阶段:
步骤 1-1:根据arguments创建并初始化AO
AO={
arguments:{
length:0
}
}
步骤 1-2:扫描函数声明,此时a指向函数声明(Function Declaration)
AO={
arguments:{
length:0
},
a: FD
}
步骤 1-3:扫描变量声明,发现AO中已经存在了a属性,则跳过,不影响已存在的属性。
AO={
arguments:{
length:0
},
a: FD
}
执行阶段:
步骤 2-1:执行第一行语句console,此时a指向的是函数声明,所以输出函数声明。
AO={
arguments:{
length:0
},
a: FD
}
步骤 2-2:执行第二句对AO中的变量对象进行赋值,所以a的值改为10。
AO={
arguments:{
length:0
},
a: 10
}
步骤 2-3:执行第三句,是函数声明,在执行阶段不会再将其添加到AO中,直接跳过。所以AO还是上次的状态。
AO={
arguments:{
length:0
},
a: 10
}
步骤 2-4:执行第四句,此时a的值是10,所以输出10。
AO={
arguments:{
length:0
},
a: 10
}
复制代码
根据以上的示例,我们已经大致明白了EC以及EC的生命周期。
同时,我们知道函数每次调用都会产生一个新的函数执行上下文。
那么,如果有若干个执行上下文呢,JavaScript是怎样执行的?
这就涉及到 执行上下文栈 的相关知识。
六、执行上下文栈
1. 术语理解
执行上下文栈(Execution context stack,ECS),简称ECS。
简单理解就是若干个执行上下文组成了执行上下文栈。也称为执行栈、调用栈。
2. 作用
用来存储代码执行期间的所有上下文。
3. 特点
我们知道栈的特点是先进后出。可以理解为瓶子,先进来的东西永远在最底部。
所以
执行上下文栈的特点就是
LIFO(Last In First Out)
也就是后进先出。
4. 存储机制
- JS首次执行时,会将全局执行上下文存入栈底,所以全局执行上下文永远在最底部。
- 当有函数调用时,会创建一个新的函数执行上下文存入执行栈。
- 永远是栈顶处于当前正在执行状态,执行完成后出栈,开始执行下一个。
5. 示例分析
我们用代码简单理解一下
示例1:
function f1(){
f2();
console.log(1)
}
function f2(){
f3();
console.log(2)
}
function f3(){
console.log(3)
}
f1(); // 3 2 1
复制代码
根据执行栈的特点进行分析:
(1)我们假设执行上下文栈是数组ECStack,则ECStack=[globalContext],存入全局执行上下文(我们暂且叫它globalStack)
(2)调用f1()函数,进入f1函数开始执行,创建f1的函数执行上下文,存入执行栈,即ECStack.push('f1 context')
(3)f1函数内部调用了f2()函数,则创建f2的函数执行上下文,存入执行栈,即ECStack.push('f2 context'),f2执行完成之前,f1无法执行console语句
(4)f2函数内部调用了f3()函数,则创建f3的函数执行上下文,存入执行栈,即ECStack.push('f3 context'),f3执行完成之前,f2无法执行console语句
(5)f3执行完成,输出3,并出栈,ECStack.pop()
(6)f2执行完成,输出2,并出栈ECStack.pop()
(7)f1执行完成,输出1,并出栈ECStack.pop()
(8)最后ECStack只剩[globalContext]全局执行上下文
示例2:
function foo(i){
if(i == 3){
return
}
foo(i+1);
console.log(i)
}
foo(0); // 2,1,0
复制代码
分析:
(1)调用foo函数,创建foo函数的函数执行上下文,存入EC,传0,i=0,if条件不满足不执行,
(2)执行到foo(1),再次调用foo函数,创建一个新的函数执行上下文,存入EC,此时传入的i为1,if条件不满足不执行,
(3)又执行到foo(2),又创建新的函数执行上下文,存入EC,此时i为2,if条件不满足不执行
(3)又执行到foo(3),再次创建新的函数执行上下文,存入EC,此时i为3,if满足直接退出,EC弹出foo(3)
(4)EC弹出foo(3)后执行foo(2)剩下的代码,输出2,foo(2)执行完成,EC弹出foo(2)
(5)EC弹出foo(2)后执行foo(1)剩下的代码,输出1,foo(1)执行完成,EC弹出foo(1)
(6)EC弹出foo(1)后执行foo(0)剩下的代码,输出0,foo(0)执行完成,EC弹出foo(0),此时EC只剩下全局执行上下文。
七、总结
- 全局执行上下文只有一个,并且在栈底。
- 当浏览器关闭时,全局执行上下文才会出栈。
- 函数执行上下文可以有多个,并且函数每调用执行一次(即使是调用自身),就会生成一个新的函数执行上下文。
JS是单线程,所以是同步执行,执行上下文栈中,永远是处于栈顶的是执行状态。VO或是AO只有一个,创建过程的顺序是:参数声明>函数声明>变量声明- 每个
EC可以抽象为一个对象,这个对象包含三个属性:作用域链、VO/AO、this






















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)