前言
学习 JavaScript ,理解其内部执行逻辑是很重要的,今天这篇文章就来讲讲 JavaScript
执行上下文
和执行栈
。
JavaScript 系列文章:JavaScript进阶
什么是执行上下文和执行栈?
执行上下文
执行上下文
就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。
有全局执行上下文
、函数执行上下文
、Eval 函数执行上下文
三种执行上下文类型。
类型 | 定义 |
---|---|
全局执行上下文 | 首次运行程序时创建的默认环境。一个程序只能有一个全局上下文 |
函数执行上下文 | 每次调用函数时,都会创建一个新的函数执行上下文 |
Eval 函数执行上下文 | 在 eval 函数中执行的代码。很少用且不建议使用,本文不做详细介绍。 |
注:
全局上下文 做了两件事:
-
定义一个全局对象,在浏览器中全局对象是
window
-
将
this
指向这个全局对象。关于 this 的详细介绍,建议去看 一文吃透五种this绑定方式
执行栈
执行栈具有栈结构(
LIFO
后进先出),用于存储在代码执行期间创建的所有执行上下文。
-
程序执行时,先创建一个全局上下文
push
到执行栈中,每调用一个函数,会为该函数创建一个执行上下文并push
到执行栈的顶部 -
当执行栈中的函数执行完成后,会把该函数从执行栈的顶部
pop
出去,然后继续运行执行栈中的下一个函数,以此类推。 -
所有程序执行完毕,Javascript 引擎把全局执行上下文从执行栈中移除。
我们来运行下案例试试
function first() {
// 调用 second 函数
second();
console.log('first() 函数执行上下文结束')
}
function second() {
console.log('second() 函数执行上下文结束')
}
// 调用函数
first();
console.log('全局执行上下文结束')
在这个案例里,执行程序时,会先创建一个全局上下文,然后先调用函数 first()
创建函数执行上下文 push 到执行栈,再调用 second()
创建执行函数上下文 push 到执行栈。因此,这个程序 push 到执行栈的顺序如下:
全局执行上下文 => first() 执行上下文 => second() 执行上下文
second()
函数执行结束后,会先行执行栈中 pop 出去,然后执行将函数 first()
从执行栈中 pop 出去,程序执行结束时,将全局执行上下文 pop 出去。案例中,从执行栈 pop 出去的顺序(LIFO
后进先出)如下:
执行上下文的生命周期
通过前面执行栈的介绍,我们理解了程序是怎么管理执行上下文的,那么执行上下文是如何创建的呢?我们接下来看看执行上下文的生命周期
执行上下文的生命周期分为 创建阶段 => 执行阶段 => 回收阶段 三个阶段
创建阶段
当函数被调用,但未执行任何其内部代码之前,都处于执行上下文的创建阶段。
创建阶段主要做变量环境(创建变量对象)
、词法环境(创建作用域链)
、this 绑定(确定 this 指向)
三件事。
1.词法环境(创建作用域链)
词法环境这一步,也可以理解为创建作用域链
。
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量与函数的关系。简单理解,就是一个包含 标识符变量映射 的结构。
词法环境由 环境记录
和 对外部环境的引用
两部分组成。
- 环境记录 存储变量和函数声明。环境记录分为
声明性环境记录
、对象环境记录
、全局环境记录
类型 | 定义 |
---|---|
声明性环境记录 | 用于记录标识符与变量的映射,只记录非var 声明的标识符(let、const、function…… ),没有关联的绑定对象 |
对象环境记录 | 用于记录标识符与变量的映射,只记录var 声明的标识符,有一个关联的绑定对象 |
全局环境记录 | 声明性环境记录 和对象环境记录 的组合 |
我们来看一个经典的问题: 暂存死区
var name = '谷底飞龙';
var f = ()=>{
console.log(name);
let name = '天下无敌';
}
f();
在函数 f
外部定义了变量 name
,在函数内部用 let
也定义了变量 name
,此时在 let
定义之前访问变量 name
时,会报未初始化的错误。
这就是一种暂存死区的现象。出现这个问题的原因是啥呢?这就跟我们讲的环境记录有关。let
定义的变量属于声明性环境记录
,没有关联的绑定对象(也就是预留了内存空间,但是没有与标识符进行绑定
),处于未初始化状态。
- 对外部环境的引用 用于访问其外部词法环境
词法环境分为
全局环境
和函数环境
两种类型
类型 | 环境记录 | 对外部环境的引用 |
---|---|---|
全局环境 | 拥有一个全局对象(window )及其关联的方法和属性以及任何用户自定义的全局变量,this 的值指向这个全局对象 | 没有外部环境,对外部环境的引用是null |
函数环境 | 用户在函数 中定义的变量被存储在函数环境记录中,还包含一个 arguments 对象 | 可以是全局环境 ,也可以是外部函数环境 |
注:
函数环境 中包含的arguments
对象包函数的参数的索引与参数的映射和参数的长度。如下面的案例的arguments
是{0:'谷底飞龙', 1: 28, length: 2}
function f(name, age){
console.log(`my name is ${name}, my age is ${age}`);
}
f('谷底飞龙', 28);
// Arguments: {0:'谷底飞龙', 1: 28, length: 2}
2.变量环境(创建变量对象)
变量环境这一步,也可以理解为创建变量对象(包含变量,函数和参数)
。
变量环境也是一个词法环境,具有词法环境的所有属性。
ES6 中,变量环境与词法环境的区别: 词法环境用于存储函数声明和变量绑定(
let
和const
);变量环境仅用于存储变量绑定(var
)
在创建阶段,代码会被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为 undefined
(在 var
的情况下)或保持未初始化(在 let
和 const
的情况下)。
- 变量提升
创建变量对象时,会依次获取执行上下文的变量声明(通过var
声明的变量),如果没有变量没有定义,会默认设置为undefined
,这就是变量提升
console.log(name); // 输出 undefined
var name = '谷底飞龙';
通过var
定义变量 name,在定义之前使用,会默认设置为undefined
。
变量提升后,相当于下面的代码
var name;
console.log(name); // 输出 undefined
name = '谷底飞龙';
但如果把 var
改成let
或者const
,变量会保持未初始化,程序会报错:
- 函数声明提升
创建一个函数的方法有两种:通过函数声明
function f(){}
或者变量声明var f = function(){}
。这两种在函数提升有什么区别呢?
console.log('通过函数声明的方式:' + f1);
console.log('通过变量声明的方式:' + f2);
// 通过函数声明的方式
function f1() {}
// 通过变量声明的方式
var f2 = function() {}
执行结果如下:
小结: 通过函数声明的方式创建函数,函数声明会被提升,可以在函数定义之前执行函数,也就是函数声明提升
;通过变量声明的方式创建函数,属于变量声明提升,默认设置为undefined
有个细节需要注意:如果函数声明和变量声明同名,函数声明提升时,函数声明的优先级高于变量声明。
// 函数定义之前执行函数,以函数声明创建的方式为主
// 输出 “执行通过函数声明创建的函数”
f();
// 通过函数声明的方式
function f() {
console.log('执行通过函数声明创建的函数')
}
// 通过变量声明的方式
var f = function() {
console.log('执行通过变量声明创建的函数')
}
// 函数定义之后执行函数,以最近创建的声明(这里是变量声明)为准
// 输出 “执行通过变量声明创建的函数”
f();
3.this 绑定(确定 this 指向)
this 绑定,实际上就是确定 this 的指向
。建议去看看我这篇文章 一文吃透五种 this 绑定方式
- 在全局执行上下文中,
this
绑定的是全局对象,在浏览器中,全局对象是window
- 在函数执行上下文中,
this
绑定的值取决于调用函数的对象。如果它被一个对象引用调用,那么this
的值被设置为该对象,否则this
的值被设置为全局对象或undefined
(严格模式下)
执行阶段
执行阶段完成对所有
变量的分配
,最后执行代码
。
在执行阶段,JavaScript 引擎如果在源代码中声明的实际位置找不到let
变量的值,那么将为其分配undefined
回收阶段
执行上下文出栈,等待虚拟机回收执行上下文
结语
耗时三天整理编写,如果你喜欢这篇文章,可以帮忙点个赞
~
欢迎关注我的公众号 「谷底飞龙」~
参考: