【2021 第二期】简而不单,单而不简的执行上下文

478 阅读9分钟

大伙儿好~,我是小鹿,公众号:「小鹿动画学编程」 原创作者。

2021 第二期。

想必大伙儿看到本期的标题很有疑惑,为什么是简而不单,单而不简的执行上下文呢?我来先解释一下,对于 javaScript 上一些抽象的概念,我们可以把它讲的非常复杂,也可以把它讲的极其简单,更可以把它讲的既复杂又简单。

嗯~ 最近重新回顾了这些抽象的概念,发现有些概念之前并不能很好的融会贯通,所以把这些相对抽象难以理解的概念作为几期文章来写。

不妨打开在谷歌搜「执行上下文」关键词,几篇相对排名靠前的优秀文章回呈现出来。

看到这几篇文章,作者尽心尽力的去解释这些比较抽象的概念,文字很多,图相对较少。我个人觉得要想更好的理解抽象概念,不得不借助可视化的图像减少作者和读者之间的理解力和理解差错。所以,不用担心,这篇文章小鹿会通过加入更多的图片深入浅出的解释这些抽象的概念。

本文章已在 Github blog 第二期 收录,欢迎大伙儿~ Star,文章中若存在不足或者 issues,欢迎在下方或 Github 留言!

本期目录

1、执行上下文

1.1 JavaScript 引擎

说到执行上下文,不得不先扯扯 JavaScript 引擎,JavaScript 引擎是什么?考虑到这篇文章不专门写 JavaScript 引擎,可以自己谷歌一下。说白了,JavaScript 引擎就是用来「解释」、「编译」和「执行JavaScript 代码的,毕竟开发人员写的 JS 代码只能够让开发者认得出来,交给计算机,由于计算机只识别二进制,所以中间需要进行一系列的解释和转化才能看懂执行这些 JavaScript 代码。

1.2 执行栈 (Execution stack)

小鹿注:保证 JavaScript 代码的执行"顺序"。

JavaScript 引擎既然可以执行 JS 的代码,那么是按照什么顺序执行的,又是怎么保证这些顺序而不被所打乱的。先看一段简单的代码:

var foo2 = function () {
  console.log('foo2');
}

var foo1 = function () {
  console.log('foo1');
  foo2()
  console.log('foo3')
}

foo1(); // 输出:“foo1  foo2  foo3”

通过上述代码片段的执行,输出的顺序为'foo1 foo2 foo3'

代码执行,foo1()函数先执行,首先输出'foo1',遇到 foo2() 函数的执行命令,将执行权交给 foo2foo2 函数体执行,输出'foo2'foo2 执行完毕后,将执行权交回 foo1 函数,最后输出'foo3'

我们可以找出上述代码执行的规律,先执行的函数,会在最后退出,后执行的函数,先执行完毕。这个执行顺序不就是“栈”的“先进后出”``“后进先出”的结构嘛。JavaScript 引擎将其这种执行结构称为「执行栈」,用于保证 JavaScript 代码的顺序。

1.3 执行上下文(Execution Context)

小鹿注:将执行的代码"模块化" —— 执行上下文的分类。

什么是执行上下文?虽然我们在“执行上下文”词义上很难直接理解,但是它具体代表的是什么,是很容易理解的,下面我把“执行上下文”的抽象概念进行具体化。

上述我们已经解释了 JavaScript 引擎是使用执行栈来保证代码的执行顺序的,但是执行过程中需要涉及到一些变量的作用范围界定(作用域)、闭包等复杂情况,我们需要 JavaScript 引擎引入一种机制来解决这些看起来复杂的问题,所以「执行上下文」的概念产生了。

但是,执行上下文是什么?这不得不让我想起组件的模块化开发,之前的一个网页应用代码从上到下一个文件写下来几千行代码,难以阅读、难以维护,所以有了后来的模块化开发。每个模块都有自己的功能,都有属于自己的局部变量和样式。

我们可以理解为 JavaScript 引擎为了更好的解释和执行代码,所以引入类似于像组件模块的“执行上下文”的概念用于管理运行时代码的复杂度。

2、执行上下文的分类

上述我们把抽象的“执行上下文”类似于“模块”的具体概念便于理解。当然,执行上下文也就是所谓的“模块”也有不同的分类,在这里具体只展开两种,「全局执行上下文」和「局部执行上下文」。

2.1 全局执行上下文(Global Execution Context)

全局上下文这个“模块”由两部分组成,「全局对象」和「this」。

下图是全局执行上下文的最基本形式。包含一个 window 对象,以及一个 this 变量,而这个 this 变量是指向 window 对象的,如最右图的打印结果。

从这里我们看出,执行上下文可以理解为是一个在内存中的「对象和变量」集合的模块(或者说是片段),这也是为什么我们可以把它看作类似“模块”的原因(除此之外还有其他作用)。

小鹿注:为了便于理解,定义是我自己总结的,如有欠缺欢迎指出~

2.2 局部执行上下文

局部执行上下文和全局执行上下文类似,但不完全相同,在函数局部执行上下文中,需要注意的有一下两点:

  • 函数传入的参数会作为局部执行上下文的变量来存储

  • 局部上下文有一个 arguments 参数对象(参考)

局部执行上下文内容会在下面的两个阶段中详细讲到。

3、执行上下文两个阶段

无论是全局执行上下文还是局部的执行上下文,都会经历两个阶段,分别是「创建」和「执行」。

如下我们有一段代码:

var name = "小鹿";
var age = 23;

function getInfo(){
  return {
    name: name,
    age: age
  };
}

3.1 创建阶段(Creation)

创建阶段要完成的事情,如下:

  • 在堆内存中创建全局对象(global object)—— 浏览器环境是 windowsNode 环境是 Global
  • this 变量指向这个全局对象
  • 设置当前执行上下文中「变量和函数」的内存空间
  • 将声明的变量加入内存中(同时挂在到全局对象上),为变量赋值 undifined,函数存储的是字符串形式

小鹿注:左 (1) 图执行的代码,左 (2) 图创建阶段完成后的执行上下文内存中状态,右 (1) 创建阶段全局对象的状态。

JavaScript 引擎在执行代码之前,先在堆内存中创建全局执行上下文,生成全局对象(global object),然后让 this 变量指向这个变量。JavaScript 发现代码中声明的两个变量 nameage,然后在全局执行上下文中申请内存空间,将变量存储到该内存空间内,然后为该变量赋值 undefined,函数就以字符串的形式存储在内存中。

小鹿注:在创建阶段为变量声明指定默认值(undefined)的过程称为「变量提升」。

3.2 执行阶段(Execution)

全局执行上下文创建完成之后,开始由创建状态(Creation)变为执行状态( Execution)。JavaScript 引擎开始逐行运行和执行代码,并为在创建阶段放入内存的变量赋予值。

小鹿注:左 (1) 图执行的代码,左 (2) 图执行阶段完成后的执行上下文内存中状态,右 (1) 执行阶段全局对象的状态。

局部执行上下文和全局执行上下文的创建和执行过程是一模一样的。但是全局执行上下文创建一次,而函数局部执行上下文是随着函数的每次调用都要创建一个局部执行上下文。

还是上述例子,执行结果如下:

var name = "小鹿";
var age = 23;

function getInfo(name){
  console.log(name);
  return {
    name: name,
    age: age
  };
}

getInfo(name);

函数局部上下文执行状态如下:

小鹿注:由于函数中没有定义新的变量,所以在这里没有变量提升。

我们了解了什么是函数局部上下文,当函数局部上下文执行完毕之后,就会执行出栈操作,将执行权交给父级执行上下文(可能是局部执行上下文,也可能是全局执行上下文),上述 getInfo 函数执行完毕的状态如下图所示。

此时的函数执行完毕,局部执行上下文出栈销毁,执行权交给全局执行上下文继续执行其他代码。

由于 JavaScript 是单线程的,一次只能执行一个任务,为了方便大伙儿理解,左(3)图 是执行栈的调用情况。当然,我们也可以发现,左(2)图是以嵌套的方式来模拟执行栈的操作,每一个嵌套选项都是堆栈中一个新的执行上下文。

4、小结

执行上下文在 JavaScript 中是一个非常重要的概念,在接下几期的进阶文章中,作用域、作用域、闭包、this等概念,都会与本期文章内容挂钩。力争从原理上理解这些抽象的概念。

说点题外话,今年为什么突然要从最基础的内容写起呢?觉得之前自学备战面试的时候,有些概念虽然记住了,但是没有做到真正的理解和融会贯通,所以打算通过每一期文章,再把之前理解不透彻的这些点都搞清楚。

❤️ 原创不易,欢迎素质三连[点赞 + 收藏 + 评论]

每一期的文章都是工作之外零碎时间整理而出,涉及到一些难懂的概念,会查阅大量的资料和自我思考,难免会出现一些笔误和不足,也欢迎各位读者批评指出~。原创不易,如果觉得不错,希望点赞多多支持~,我们下期再见~

我是小鹿,文章同步更新 Github,也可以在微信搜一搜「小鹿动画学编程」第一时间接收文章更新通知,回复“前端”可获取小鹿整理的备战面试小册。