什么是执行上下文?
简而言之,执行上下文就是JavaScript代码执行的环境的抽象概念。每当JavaScript代码在运行的时候,他都是在执行上下文中运行。
执行上下文的类型
- 全局执行上下文,这是默认或者基础的环境,任何不在函数中执行的代码,都会在基础环境中执行。在基础环境中执行会有两步:创建一个全局的
window对象(浏览器环境中),并且设置this的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 - 函数执行上下文,每当一个函数被调用的时候,都会为该函数创建一个执行的环境,也就是执行上下文,每个函数都有自己的执行上下文,且都是在被调用的时候创建的。函数上下文可以有多个,创建的时候压入栈,执行完出栈。
Eval函数执行上下文,执行在eval函数内部的代码也有自己的执行上下文,但由于JavaScript开发不经常使用eval,所以不谈论,单需要有人家奥~
理解上,我把全局执行上下文比作这个世界,世界上有好多小房子,每个小房子就是函数执行上下文,我们不管做什么事都会在世界里这个基本的环境里,当进入每一个小房子就相当于有了一个新的环境,这个时候要是做一些操作,就相当于在小房子这个新环境里了。可能还有些偏差。想想还有什么更通俗易懂的解释方法,毕竟看了好久官方解释才看懂哇~
执行栈
执行栈,就是其他编程语言中“调用栈”,说到底还是数据结构中的“先进后出”的栈,对应的是队列,是用来存储代码执行时的所创建的所有执行上下文,也就是所有环境。
当JavaScript引擎第一次遇到脚本时,会先创建一个全局的执行上下文环境,并且压入当前的执行栈。每当引擎遇到一个函数时,都会创建一个函数的执行上下文,也是压入当前的执行栈的顶部,当函数执行完毕时,该执行上下文出栈。
JavaScript执行代码并不是说一行一行的执行,更准确点说应该是一段一段的顺序执行。
上面所说的都是JavaScript如何管理执行栈,那JavaScript引擎是如何创建一个执行上下文的?
如何创建一个执行上下文
创建一个执行上下文有两个阶段:创建阶段 和 执行阶段
创建阶段
创建阶段一共经历3步操作:
- 确定this值的绑定
- 创建 词法环境(LexicalEnvironment) 组件
- 创建 变量环境(VariableEnvironment) 组件
下面我们详细的看一下每一步都具体做什么操作:
this值绑定
- 全局执行上下文中,this指向全局对象,浏览器中就是
window对象,而在nodejs中就是这个文件的module对象 - 函数执行上下文中,当然就是指向最后调用函数那个对象
词法环境(LexicalEnvironment)
词法环境包含两部分:
- 环境记录器:存储变量和函数声明的具体位置
- 对外部环境的引用:可以访问的外部的词法环境
词法环境包含两种类型:
- 全局词法环境,全局环境的外部环境的引用是null,拥有内建的原生对象,比如:
Object、Array等,环境记录器中记录着全局对象还有任何用户定义的一些变量,this值指向全局对象。 - 函数词法环境,环境记录器存储着函数内部的一些变量,外部环境引用可能是全局对象,也可能是包含该函数的外部词法环境。
环境记录器也包含两种类型:
- 对象环境记录器(全局环境中)
- 声明式环境记录器(函数环境中)
在函数环境,声明式环境记录器还包含一个传递给函数的arguments对象(此对象存储索引和参数的映射)和传递给函数的参数的length
下面用一张图整理表示就是:
用伪代码表示词法环境的话:
//全局执行上下文
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: 'Object',
//标识符
},
outer: <null>
}
}
//函数执行上下文
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: 'Declarative',
Arguments: { length }
//标识符
},
outer: <Global or outer function environment reference>
}
}变量环境(VariableEnvironment)
变量环境说到底也是一个词法环境,环境记录器同样记录着执行上下文创建的时候变量声明的绑定关系。
词法环境与变量环境唯一的区别就是:
- 词法环境的环境记录器存储的是存储函数声明和变量(
let和const类型) - 变量环境存储的是变量的绑定(
var类型)
代码理解
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);<!--全局执行上下文-->
<!--词法环境-->
<!--环境记录器-->
<!--Type-->
<!--a-->
<!--b-->
<!--multiply-->
<!--外部环境引用-->
<!--变量环境-->
<!--环境记录器-->
<!--Type-->
<!--c-->
<!--外部环境引用-->
<!--函数执行上下文-->
<!--词法环境-->
<!--环境记录器-->
<!--Type-->
<!--Arguments-->
<!--外部环境引用-->
<!--变量环境-->
<!--环境记录器-->
<!--Type-->
<!--g-->
<!--外部环境引用-->//全局执行上下文
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: 'Object',
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
},
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: 'Object',
c: undefined
},
outer: <null>
}
}
//函数执行上下文
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: 'Declarative',
Arguments: { 0: 20, 1: 30, length: 2 }
},
outer: < GlobalLexicalEnvironment >
},
VariableEnvironment: {
EnvironmentRecord: {
Type: 'Declarative',
g: undefined
},
outer: < GlobalLexicalEnvironment >
}
}只有遇到multiply函数被调用,函数执行上下文才会被创建
那为什么let和const类型并没有被初始化,而var类型的变量被赋undefined?
这是因为在变量词法环境中,标识符的对应关系是undefined而不是未初始化
这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。
也就是所谓的变量提升
执行阶段
执行阶段应该比较好理解。
当JavaScript引擎遇到一段脚本,首先会看一遍这段代码有什么变量,这些变量无论是基础类型的还是引用类型或者函数类型,也就是上面的创建阶段。
后面就是从上往下依次执行代码,包括变量赋值或者函数执行,创建函数执行上下文。
- 全局上下文的变量对象初始化是全局对象
- 函数上下文的变量对象初始化只包括 Arguments 对象
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
- 在代码执行阶段,会再次修改变量对象的属性值
结语
这只是执行上下文相关内容的一个开篇,抽时间来不断的充实自己是一个非常重要的过程,不要被业务覆盖了我们美好的生活,加油吧,少(sao)年----❤