一、什么是执行上下文
简单说就是代码运行时的执行环境,必须是在函数调用的时候才会产生,如果不调用就不会产生这个执行上下文。在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。代码分为三类:全局代码、局部(函数)代码、Eval代码(先不考虑这个),那么也就有三种执行环境,全局执行上下文、函数执行上下文、eval。如图所示:
全局执行上下文
* 在执行全局代码前将window确定为全局执行上下文
* 对全局数据进行预处理
* var定义的全局变量==>undefined, 添加为window的属性
* function声明的全局函数==>赋值(fun), 添加为window的方法
* this==>赋值(window)
* 开始执行全局代码
函数执行上下文
* 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
* 对局部数据进行预处理
* 形参变量==>赋值(实参)==>添加为执行上下文的属性
* arguments==>赋值(实参列表), 添加为执行上下文的属性
* var定义的局部变量==>undefined, 添加为执行上下文的属性
* function声明的函数 ==>赋值(fun),
* this==>赋值(调用函数的对象)
* 开始执行函数体代码
二、执行上下文栈
1.在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象,每次调用一个方法A的时候,这个方法可能也会调用另一个方法B,B还可能调用方法C,而JS只能同时一件事,所以
方法B、C没执行完之前,方法A也不能被释放,那总得找个地方把这些方法按顺序存一存吧,存放的地方就是执行上下文栈,也叫调用栈。
1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈),(最底部)
3. 在函数执行上下文创建后, 将其添加到栈中(压栈)
4. 在当前函数执行完后,将栈顶的对象移除(出栈)
5. 当所有的代码执行完后, 栈中只剩下window
以下面这个函数举例子:
画的有点丑
这个js代码整体有三个执行上下文,window、bar()函数、foo()函数,js代码在第一次加载的时候会先创建一个全局的window执行上下文,如果代码中有函数调用,就创建一个该函数的执行上下文,并将这个上下文推入到执行栈顶,在当前bar函数中又调用了一个函数,继续创建foo函数的执行上下文,并将该上下文推入栈顶,浏览器会一直执行当前栈顶的执行上下文,一旦函数执行完毕,该上下文就会被推出执行栈,直到最后剩一个全局执行上下文。如上图。
思考:假如bar函数中同时调用了两个函数foo1和foo2,那么这种情况下,栈中会有几个执行上下文?
依然是只有三个,因为执行foo1函数的时候foo2函数并不会推入栈中,直到foo1函数执行完毕,此时foo1会被推出栈,再加入foo2函数,,所以要牢记,函数执行完就会被弹出栈。
三、执行上下文的三个阶段
1.创建阶段
(1).生成变量对象
(2).建立作用域链
(3).确定 this 指向
在全局执行上下文中,this总是指向全局对象,例如浏览器环境下this指向window对象。
而在函数执行上下文中,this的值取决于函数的调用方式,如果被一个对象调用,那么this指向这个对象。否则this一般指向全局对象window或者undefined(严格模式)。
2.执行阶段
(1).变量赋值
(2).函数引用
(3).执行其他代码
3.销毁阶段
执行完毕出栈,等待回收被销毁
以下面这个函数作为例子:
console.log(this)
function fn (a1) {
console.log(a1)
console.log(a2)
a3()
console.log(this)
console.log(arguments)
var a2 = 4
function a3 () {
console.log('a3()')
}
}
fn(2, 3) //调用
-
首先JS解释器(引擎)开始解释代码,构建执行环境栈(Execution Context Stack),并根据执行环境的不同生成不同的执行上下文(Execution Context)
-
栈底永远是全局上下文,当遇到fn(),确认调用函数,就创建生成fn自己的上下文,然后将函数执行上下文入栈(push on)到执行环境栈中。
-
当函数执行完,弹出栈,销毁
再来看一个例子:
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
我们用伪代码来描述上述代码中执行上下文的创建过程:
//全局执行上下文
GlobalExectionContext = {
// this绑定为全局对象
ThisBinding: <Global Object>,
// 词法环境
LexicalEnvironment: {
//环境记录
EnvironmentRecord: {
Type: "Object", // 对象环境记录
// 标识符绑定在这里 let const创建的变量a b在这
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
// 全局环境外部环境引入为null
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object", // 对象环境记录
// 标识符绑定在这里 var创建的c在这
c: undefined,
}
// 全局环境外部环境引入为null
outer: <null>
}
}
// 函数执行上下文
FunctionExectionContext = {
//由于函数是默认调用 this绑定同样是全局对象
ThisBinding: <Global Object>,
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 声明性环境记录
// 标识符绑定在这里 arguments对象在这
Arguments: {0: 20, 1: 30, length: 2},
},
// 外部环境引入记录为</Global>
outer: <GlobalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 声明性环境记录
// 标识符绑定在这里 var创建的g在这
g: undefined
},
// 外部环境引入记录为</Global>
outer: <GlobalEnvironment>
}
}
在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined,函数被设置为了自身函数,而let const被设置为未初始化。
现在你总知道变量提升与函数提升是怎么回事了吧,以及为什么let const为什么有暂时性死域,这是因为作用域创建阶段JS引擎对两者初始化赋值不同。
上下文除了创建阶段外,还有执行阶段,这点大家应该好理解,代码执行时根据之前的环境记录对应赋值,比如早期var在创建阶段为undefined,如果有值就对应赋值,像let const值为未初始化,如果有值就赋值,无值则赋予undefined。
五、练习
想想这段代码的输出顺序是什么
console.log('gb: '+ i)
var i = 1
foo(1)
function foo(i) {
if (i == 4) {
return
}
console.log('fb:' + i)
foo(i + 1) //递归调用: 在函数内部调用自己
console.log('fe:' + i)
}
console.log('ge: ' + i)
这是一个递归调用,在不满足条件的情况下函数会一直调用自己,直到条件满足函数就结束调用,继续上图:
很多人都觉得fe输出3就结束了,怎么还会有2和1,其实return只是结束了最后一次调用,没有执行完的代码会按出栈顺序执行完。
所以依次输出为:
gb: undefined
fb: 1
fb: 2
fb: 3
fe: 3
fe: 2
fe: 1
ge: 1