前言
执行上下文的概念在JavaScipt中是颇为重要的。想要更好的掌握JS,就要去了解在执行JS代码时候,它背后都做了哪些事情,把原理概念搞清楚,才可以更好的掌握基于执行上下文的其它知识点,我认为JS的知识并不是分散的,而是可以通过一个知识点链接到另外一个知识点,它们之间并不是完全隔离分散开的。
那么我想,通过了解JavaScript的执行上下文,一定可以让你搞明白,
JS执行流程
,变量提升
,作用域链
,this
相关知识,具体的知识点并不会详细说明,更多的想要让你通过理解执行上下文而搞懂这些知识点的背后原理
一:什么是执行上下文
1.1 组成
执行上下文是JavaScript执行一段代码时的运行环境
,就例如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的this,变量,对象以及函数等。(类似于初始化时候确定相关值)
只要js在执行中,那它一定是运行在执行上下文中
简单粗暴直接上图,这就是执行上下文中所包含的部分
变量环境
:用于存储变量声明(包括函数声明)和函数参数- 环境记录 :它记录了函数内的变量、函数参数以及由
var
声明的变量 - 外部环境引用:指向包含当前变量环境的外部词法环境,形成了变量对象的作用域链
- 环境记录 :它记录了函数内的变量、函数参数以及由
词法环境
:存储变量的词法作用域信息。它与变量环境紧密相关,但在一些特殊情况下(例如闭包),词法环境可以和变量环境有所不同- 环境记录 :它记录了函数的变量、函数参数以及由
let
、const
和class
声明的变量 - 外部环境引用:指向包含当前词法环境的外部词法环境,形成了词法作用域链
- 环境记录 :它记录了函数的变量、函数参数以及由
this
:定义了当前执行代码的上下文对象。在函数上下文中,this 值取决于函数是如何被调用
的外部引用(outer)
: 指向包含当前执行上下文的外部词法环境。这是实现词法作用域链的关键
注意
:执行上下文中的外部引用(outer)和变量环境,词法环境中的外部环境引用本质上没有区别,变量环境中的外部环境引用更加具体地描述了在变量环境中用于指向外部词法环境的引用
1.2 类型
这里不描述eval执行上下文
- 全局执行上下文:红宝书中描述说:`全局上下文是最外层的上下文,在浏览器中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用let和const的顶级声明不会定义在全局上下文中
- 函数执行上下文:存在无数个函数执行上下文,但是只有函数被调用时候才会创建函数执行上下文。函数执行上下文包含函数作用域范围内的变量,函数参数,函数和作用域链。
- eval执行上下文: 指的是运行在
eval
函数中的代码,很少用而且不建议使用
示例
1.3 执行栈(调用栈)
执行上下文的栈结构(调用栈)用于跟踪代码的执行顺序,并确保上下文按照正确的顺序进入和离开
看一段代码就理解了
JS
function greet(name) {
return "Hello, " + name + "!";
}
function welcome() {
return "Welcome to the execution stack!";
}
function sayHello() {
var greeting = greet("Alice");
console.log(greeting);
console.log(welcome());
}
sayHello();
当执行到
js
var greeting = greet("Alice");
- greet函数执行上下文被加入到执行栈中并被执行,执行结束后再被弹出执行栈
当执行到
js
console.log(welcome());
- welcome函数执行上下文被加入到执行栈中并被执行,执行结束后再被弹出执行栈
welcome函数执行结束之后,sayHello函数也执行结束了,两个函数执行上下文弹出执行栈,全局函数也执行结束,全局执行上下文也就被弹出执行栈
1.3 生命周期
执行上下文的生命周期为:创建阶段 ---> 执行阶段 ---> 销毁阶段
1.3.1 创建阶段
每当函数被调用时,就会创建一个新的执行上下文,并将其推入执行栈的顶部。
- 创建变量对象(Variable Object)或词法环境: 在全局执行上下文中,这通常是全局对象(例如
window
对象)。 - 建立作用域链: 外部环境引用(Outer Environment Reference)被设置为指向包含该执行上下文的外部词法环境,从而构建起作用域链
- 初始化 this 值: 对于函数执行上下文,this 值被初始化,取决于函数的调用方式。
js
function sayHello() {
var a = 10
let c = 30
function add() {
var b = 20
}
return a + c
}
sayHello();
在创建阶段
就会以上三个部分,可以理解为执行上下文的初始化
在这里就涉及到变量和函数提升
的问题
- 在创建执行上下文中会把函数和var声明的变量存放到变量环境中,
函数会存放对应函数的引用
,变量会被设置为undefined
。等到执行的阶段时候,就会从变量环境中去找变量和函数。(这就是变量提升) - 对于
let
声明的变量,会在词法环境中创建一个标识符,并将其初始化为uninitialized
(未初始化),此时,该变量进入了暂时性死区。 - outer外部环境应用会指向当前函数所在的外部环境,window
1.3.2 执行阶段
执行阶段:在创建完成之后,会进入执行阶段
- 代码逐行进行执行,条件判断,语句赋值等都会进行执行
- 如果遇到函数调用,则会创建函数执行上下文,并且将该函数的执行上下文压入到
执行栈
中
- 变量依次被赋值,this值也取决于函数是如何被调用的,当前sayHello函数在window下执行,因此this值指向window。具体关于this在之后的一篇文章会具体介绍
1.3.3 销毁阶段
- 当代码块执行完毕或函数执行完毕时,执行上下文进入执行结束和销毁阶段。
- 在这个阶段,局部变量通常会被销毁,内存资源得到释放。
二:作用域链
外部引用(outer)
: 指向包含当前执行上下文的外部词法环境。这是实现词法作用域链的关键
js
function bar() {
console.log(myName) //'x'
}
function foo() {
var myName = "xxx"
bar()
}
var myName = "x"
foo()
foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文? 词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
三:块级作用域
ES6 是支持块级作用域的,当执行到代码块时,如果代码块中有 let 或者 const 声明的变量,那么变量就会存放到该函数的词法环境中
js
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
创建执行上下文
- var 声明的变量添加到变量环境中
- let声明的变量添加到词法环境中
执行代码
当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量
沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出
参考文章: