深入理解JavaScript-执行上下文与调用栈

简介: 深入理解JavaScript-执行上下文与调用栈

前言


在说一个概念前,我们需要确定它的前提,此文以 ECMAScript5 为基础撰写


一句话解释


执行上下文就是一段代码执行时所带的所有信息


执行上下文是什么


《重学前端》的作者 winter 曾经对什么是执行上下文做过这样的解释:


JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文


并且他整理出在不同 ECMAScript 版本中执行上下文所代表的含义:


执行上下文在 ES3 中,包含三个部分。

  • scope:作用域,也常常被叫做作用域链
  • variable object:变量对象,用来存储变量的对象
  • this value:this 值

在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改成下面这个样子

  • lexical environment:词法环境,当获取变量时使用
  • variable environment:变量环境,当声明变量时使用
  • this value:this 值

在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容

  • lexical environment:词法环境,当获取变量或者 this 值时使用
  • variable environment:变量环境,当声明变量时使用
  • code evaluation state: 用于恢复代码执行位置
  • Function:执行的任务是函数时使用,表示正在被执行的函数
  • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码
  • Realm:使用的基础库和内置对象实力
  • Generator:仅生成器上下文有这个属性,表示当前生成器


总结的很完整,按照 选新不选旧 原则,本文应该以 ES2022 为切入点展开,最次也要 ES2018,但主流的解释执行上下文都以 ES3/ES5 为例,权衡之后,笔者将以 ES5 为基础撰写执行上下文,并在后续补充说明 ES3 中的执行上下文


执行生命周期


我们在讲 词法环境 时,曾经画过一张执行生命周期图,当时所讲的词法环境是在(预)编译阶段产生,现在讲的执行上下文是在引擎执行阶段进行


一段代码如果要执行,首先会往调用栈(call stack)中压入全局执行上下文;再创建词法环境,此时变量该提升提升,函数该提升提升,并将这些变量登记到词法环境中(编译阶段);接着进入执行阶段,执行可执行代码,该赋值赋值,遇到函数,就创建一个函数执行上下文,并往调用栈中压入该函数的执行上下文;而后创建该函数的词法环境,当该函数执行完后,从调用栈中弹出;反复循环,到最后调用栈中只剩一个全局执行上下文,除非你关闭浏览器,不然全局执行上下文不会弹出


我们要往调用栈中压入执行上下文,调用栈的数据结构为 。特点为先进后出

如果我们的代码是这样的


var a = 1;
function foo() {
    function bar() {
        console.log(a);
    }
    bar();
}
function baz() {
    foo();
}
baz();


那么执行过程应该是这样:

image.png

图中的蓝色方块为 执行上下文 ,外面黑框白底的区域就是模拟 调用栈。整个过程遵循先进后出的原则


  • 在任何代码执行之前,先创建全局执行上下文,并往调用栈中压栈(编译阶段)
  • 创建词法环境,登记函数声明和变量声明(编译阶段)
  • 引擎执行到 baz() ,创建 baz() 的函数执行上下文,并往调用栈中压栈
  • 函数 baz() 调用 foo() ,创建 foo() 执行上下文,并将其压入调用栈中,
  • 函数 foo() 调用 bar() ,创建 bar() 执行上下文,并将其压入调用栈,
  • 函数 bar() 执行 console.log() ,同理将其压入调用栈,
  • 执行完 console.log() 后,被弹出
  • 函数 bar() 执行完毕,弹出调用栈
  • 函数 foo() 也执行完毕,弹出调用栈
  • 函数 baz() 同样执行完毕,弹出调用栈


只剩下全局执行上下文,留在栈底

在这里,我们看到 console.log() 也被压入到执行栈中,不禁有个思考,哪些代码元素会被执行到调用栈中呢?


可执行代码


事实上,不仅仅是 function 可以作为执行上下文在执行栈中运行,在 JavaScript 里定义了四种可执行代码:

  • global code:整个 js 文件
  • function code:函数代码
  • module:模块代码
  • eval code:放在 eval 的代码


所以才会看到 console.log() 被压入调用栈中,因为它属于 global code


执行步骤


JavaScript 引擎是按照可执行代码来执行代码的,每次执行步骤如下:

  1. 创建一个新的执行上下文(Execution Context)
  2. 创建一个新的词法环境(Lexical Environment)
  3. 把 LexicalEnvironment 和 VariableEnvironment 指向新创建的词法环境
  4. 把这个执行上下文压入执行栈并成为正在运行的执行上下文
  5. 执行代码
  6. 执行结束后,把这个执行上下文弹出执行栈


如何创建执行上下文


到现在,我们已经知道 JavaScript 是如何管理执行上下文的,现在让我们了解一下 JavaScript 引擎是怎样创建执行上下文的

创建执行上下文有两个阶段:1) 创建阶段2) 执行阶段


创建阶段


在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生以下三件事:

  • this 值的确定,即我们所熟知的 this 绑定
  • 创建词法环境组件(LexicalEnvironment component )
  • 创建变量环境组件(VariableEnvironment component )


所以执行上下文在概念上表示如下:

ExecutionContext = {
    ThisBinding = <this value>,
    LexicalEnvironment = { ... },
    VariableEnvironment = { ... },
}


这里需要再多嘴一句:

在很多文章中我们看到执行上下文只有 LexicalEnvironment 和 VariableEnvironment ,并没有 this。那是因为在 ES2018 后,this 就归纳到 LexicalEnvironment (如上文 winter 所说),但本文是以 ES5 为基础撰写,故此版本的执行上下文中是有 this 的


this 绑定

this 的指向很简单,谁调用我,我只想谁

在执行上下文中,this 就指向那个调用者


词法环境组件 和 变量环境组件

这两”姐妹“有点像,只是分工不同。

变量环境组件(VariableEnvironment component ) 用来登记 varfunction 等变量声明

词法环境组件(LexicalEnvironment component ) 用来登记 letconstclass 等变量声明

按上例可以这么画图:

image.png

LexicalEnvironmentVariableEnvironment 则都是词法环境(Lexical Environment)。很多文章中常把 LexicalEnvironment 理解成 词法环境,这是不对的,LexicalEnvironment 是一个单词,表示执行上下文中的是标识 letconstclass 等变量声明,而 VariableEnvironment 则是标识 varfunction 等变量声明


如果非要用中文来表示 LexicalEnvironment 的话,我更愿意用 词法环境组件 来表示;同理,VariableEnvironment 则用 变量环境组件 来表示

对我而言,变量环境组件和词法环境组件就好比”装黄豆瓶“和”装绿豆瓶“,一个负责装黄豆(var,function),一个负责装绿豆(let,const,class),它们指向词法环境,本质是从词法环境中拿数据


所以无论是词法环境组件还是变量环境组件,都有一个环境记录器和一个 outer 对象,其中环境记录器记录变量,outer 指向父级作用域

具体可以看 ECMAScript 6 标准中的第八节[1]  详细了解一下

回头看上述例子:

image.png

bar() 函数中的 console.log(a) ,本环境记录器中找不到,就引着 outer 找,在 window 中找到变量 a ,赋值后,弹出,foo() 执行完后也弹出,baz() 执行完弹出,留下全局执行上下文在栈底


之前在说 词法环境 时,我们曾下过这样的定义:outer 就是指向词法环境的父级词法环境(作用域)


但是这里就有个疑惑了,outer 既然指词法环境的父级作用域,那作用域链从那里来?

以上面的 demo 为例,bar 的执行上下文的伪代码:


BarExecutionContext = {
    ThisBinding = <Global Object>,
    LexicalEnvironment = {
     EnvironmentRecord: { ... },
        outer: <FooLexicalEnvironment>
 },
    VariableEnvironment = {
        EnvironmentRecord: { ... },
        outer: <FooLexicalEnvironment>
    },
}


bar 的 outer 指向 foo 执行上下文, foo 的 outer 指向 window,变量先从当前执行作用域查找变量,如果找不到,就引着 outer 继续查找。这一过程中,bar 作用域—foo 作用域—window 作用域。


当代码要访问一个变量时——首先会搜索当前词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境

而我们潜意识中的”作用域链“是在 ES3 中的才有的,因为那个时候的执行上下文中有 scope(作用域),当前作用域找不到变量,就往外层作用域中找,然后再到更外层的作用域找,知道全局作用域,作用域与作用域之间以链表的形式连接着,这就是作用域链


执行上下文有几种


分三种

  • 全局执行上下文
  • 函数执行上下文
  • eval 执行上下文


ECMAScript3 中的执行上下文


如果你喜欢看有关执行上下文的文章,应该常看到这样的描述,执行上下文由变量对象(Variable object,VO)、作用域链(Scope chain) 、this 构成。

其实我们可以把变量对象(VO) 看成是 ES5 中的词法环境,scope 为词法环境中的 outer


如何追踪执行上下文栈


例如:

function foo1() {
    foo2();
}
function foo2() {
    foo3();
}
function foo3() {
    foo4();
}
function foo4() {
    console.lg('foo4');
}
foo1();


得到错误提示如图:

image.png

或者在 Chrome 中执行代码,打断点得到:

image.png


关于变量对象与活动对象


笔者最开始了解执行上下文、执行上下文栈以及闭包时,大家是用变量对象和活动对象来解释的,这是 ES3 时的词汇,而本文是以 ES5 为标准展开


其两者的涵义简单来说,变量对象(Variable object)是与执行上下文相关的对象,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(activation object,AO)来表示变量对象


作用域在(预)编译阶段确定,但是作用域链是在执行上下文的创建阶段完成生产的。因为函数在调用时,才会开始创建对应的执行上下文。执行上下文包括了:变量对象、作用域链以及 this 的指向


总结


执行上下文可以理解为函数的执行环境,当函数执行时,都会创建一个执行环境

每次只能有一个执行上下文处于运行状态,因为 JavaScript 是单线程语言,它由执行栈或(叫)调用栈来管理


创建一个函数,就生成了一个作用域;调用一个函数,就生成一个作用域链

执行上下文创建阶段分为绑定 this,创建词法环境,变量环境三步

调用函数时,创建一个新的词法环境


词法环境这个说法,是 ES5 规范中的内容,可以理解为 ES3 中的变量对象,scope 为 词法环境中的 outer


在 ES5 中,词法环境变量环境 的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 var 变量绑定

在 ES3 时,执行上下文包括了变量对象、作用域链以及 this

在 ES5 时,执行上下文则包括词法环境、变量环境、this。其中词法环境或者变量环境都是由用于环境记录器和 outer 对象组成,其中 outer 指向父级作用域,环境记录器记录自由变量

image.png


Q&A


Q:是不是说在定义时确认了作用域,在调用时确认了作用域链?

A:yes,这里需要注意的是每一个执行上下文都会进行提升操作

Q:ES5 中的执行上下文的词法环境和编译阶段的词法环境有什么不同?

A:一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进行执行阶段,大致流程如下:

image.png

参考资料


  • 理解 JavaScript 中的执行上下文和执行栈[2]
  • Understanding Execution Context and Execution Stack in JavaScript[3]
  • 官方 ES[4]
  • JavaScript 执行上下文 · 变量对象
  • 老司机也会在闭包相关知识点翻车(上)[5]
  • 一篇文章看懂 JS 执行上下文[6]


[1] ECMAScript 6 标准中的第八节: https://262.ecma-international.org/6.0/

[2] 理解 JavaScript 中的执行上下文和执行栈: https://github.com/xitu/gold-miner/blob/master/TODO1/understanding-execution-context-and-execution-stack-in-javascript.md

[3] Understanding Execution Context and Execution Stack in JavaScript: https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0

[4] 官方 ES: https://262.ecma-international.org/6.0/

[5] 老司机也会在闭包相关知识点翻车(上): https://gitbook.cn/gitchat/column/5c91c813968b1d64b1e08fde/topic/5c99a9a3ccb24267c1d01960

[6] 一篇文章看懂 JS 执行上下文: https://www.cnblogs.com/echolun/p/11438363.html

相关文章
|
6月前
|
自然语言处理 JavaScript 前端开发
深入理解JS的执行上下文、词法作用域和闭包(中)
深入理解JS的执行上下文、词法作用域和闭包(中)
|
6月前
|
存储 自然语言处理 JavaScript
深入理解JS的执行上下文、词法作用域和闭包(上)
深入理解JS的执行上下文、词法作用域和闭包(上)
|
自然语言处理 JavaScript 前端开发
带你读《现代Javascript高级教程》——执行上下文与闭包(1)
带你读《现代Javascript高级教程》——执行上下文与闭包
|
存储 自然语言处理 JavaScript
带你读《现代Javascript高级教程》二、执行上下文与闭包(2)
带你读《现代Javascript高级教程》二、执行上下文与闭包(2)
136 0
|
自然语言处理 JavaScript 前端开发
带你读《现代Javascript高级教程》——执行上下文与闭包(3)
带你读《现代Javascript高级教程》——执行上下文与闭包
|
缓存 JavaScript 前端开发
带你读《现代Javascript高级教程》二、执行上下文与闭包(3)
带你读《现代Javascript高级教程》二、执行上下文与闭包(3)
164 0
|
1月前
|
存储 JavaScript 前端开发
深入理解 JavaScript 执行上下文与 this 绑定机制
JavaScript 代码执行时,会为每段可执行代码创建对应的执行上下文,其中包含三个重要属性:变量对象、作用域链、和 this。本文深入剖析了执行上下文的生命周期以及 this 在不同情况下的指向规则。通过解析全局上下文和函数上下文中的 this,我们详细讲解了 this 的运行期绑定特性,并展示了如何通过调用方式影响 this 的绑定对象。同时,文中对箭头函数 this 的特殊性以及四条判断 this 绑定的规则进行了总结,帮助开发者更清晰地理解 JavaScript 中的 this 行为。
72 8
深入理解 JavaScript 执行上下文与 this 绑定机制
|
15天前
|
自然语言处理 JavaScript 前端开发
如何在 JavaScript 中创建执行上下文
在JavaScript中,每当执行一段代码时,都会创建一个执行上下文。它首先进行变量、函数声明的创建和内存分配(即变量环境和词法环境的建立),接着进入代码执行阶段,处理具体逻辑。
|
15天前
|
存储 自然语言处理 JavaScript
如何在 JavaScript 中创建执行上下文
在JavaScript中,作用域链是一套用于查找变量和函数的机制,由当前执行上下文的变量对象和所有外层执行上下文的变量对象组成。它包括全局作用域、函数作用域和块级作用域。作用域链的工作原理是从内向外逐层查找变量,直至全局作用域。闭包通过作用域链记住其词法作用域,即使在外部作用域之外执行也能访问内部变量。作用域链有助于变量隔离、模块化和数据隐藏,提高代码的可维护性和可读性。
|
5月前
|
弹性计算 自然语言处理 JavaScript
彻底明白js的作用域、执行上下文
彻底明白js的作用域、执行上下文