JavaScript从编译到执行

524 阅读9分钟

作为Web开发人员,我们知道,JavaScript(JS)是一种具有函数优先特性的轻量级、解释型或者说即时编译型的编程语言。

在计算机技术中,即时编译(英语:Just-in-time compilation,缩写为JIT;又译及时编译、实时编译),也称为动态翻译或运行时编译,是一种执行计算机代码的方法,这种方法设计在程序执行过程中(在执行期)而不是在执行之前进行编译。通常,这包括源代码或更常见的字节码到机器码的转换,然后直接执行。实现JIT编译器的系统通常会不断地分析正在执行的代码,并确定代码的某些部分,在这些部分中,编译或重新编译所获得的加速将超过编译该代码的开销。

当一段JavaScript代码运行时,主要分为两个步骤:编译和执行。

compile.jpg

从编译到执行

JavaScript 被编译时会发生什么?创建执行上下文。当执行上下文准备就绪时,执行步骤开始。执行上下文由几部分组成。为了理解它,我们关注四个组成部分:

  • 变量环境(Variable environment)
  • 词法环境(Lexical environment)
  • 外部引用环境(Reference to the outer environment)
  • This绑定(This binding)

所有可执行的 JavaScript 代码都是逐行运行的。当执行步骤时,浏览器会逐行运行 JavaScript。同时,每当一行完成运行时,执行上下文就会更新。

execution.jpg

变量和变量环境

var num = 10
console.log(num)

当这段 JavaScript 运行时,第一步是编译,将创建执行上下文。同时,变量 num 被声明为值 undefined 并存储在变量环境中。编译步骤结束,执行步骤开始。当执行第一行时, num 变量被分配一个数字 10,变量环境同时更新。

然后执行第二行。控制台开始在其变量环境中查找 num 变量,并找到 num 。控制台打印 10 。整个过程结束,整个执行上下文被删除。从这些示例中,我们可以得出以下关键结论,变量赋值实际上分为两个步骤:

  1. 编译步骤负责变量声明。
  2. 执行步骤执行变量的赋值和其余代码。

变量提升

console.log(num)
var num = 10

这段代码与上一段代码略有不同。它在声明变量之前记录 num 变量。我们知道控制台会打印 undefined ,但是让我们从执行上下文的角度一步一步地看一下。在编译步骤中,会跳过第 1 行,因为它与变量声明无关。然后在变量环境中创建第2行中的 num 变量,编译结束。

在第一行中,控制台开始在其变量环境中查找 num 变量。此时, num 持有值 undefined 。控制台打印 undefined 。然后执行第二行,并使用值 10 更新 num 变量。整个过程结束。我们称这个过程为提升,因为变量 num 感觉就像被提升到了顶部。下面的伪代码模拟了提升效果。

然而,从执行上下文的角度来看,没有任何东西被提升。发生这种情况是因为变量是在编译步骤中声明的,托管命名基于结果。

函数提升

函数略有不同,因为我们有两种声明函数的选项。

showNumber()
showName()

// 函数表达式
var showName = function() {
  console.log('Hi, this is my name.')
}

// 函数声明
function showNumber() {
  console.log('Hey, show a number.')
}

声明 showName 函数时会发生赋值。相比之下, showNumber 函数是一个没有赋值的声明。有趣的是,我们将看到控制台日志Hey, show a number.,后面跟着一条错误消息showName is not a function.。让我们回顾一下这两个步骤,看看发生了什么:

  1. 在编译阶段, showNumber 是一个声明,因此此时它实际上被存储。
  2. 当涉及到 showName 函数时,它和 undefined 一起被赋值,因为它是一个赋值。
  3. showName 功能块直到执行步骤才被分配。

两个不寻常的情况

第一种情况是命名冲突。

如果我们使用相同的变量名,后一个变量将覆盖第一个变量名。

showNumber()

function showNumber() {
  console.log(`I'm a declaration`)
}

var showNumber = function () {
  console.log(`I'm an assignment`)
}

控制台会记录消息I’m a declaration,尽管该变量是稍后声明的。第二个 showNumber 变量不会覆盖第一个变量。当函数和变量具有相同名称时,变量声明在编译步骤中将被忽略。换句话说,function 具有优先级。这是一个很容易被忽视的陷阱,所以我们应该避免使用相同的变量名。

让我们看另一个情况。

console.log(num)

if (0) {
  var num = 10
}

if 中的条件是 0 ,它是一个负值。 if 块中的代码永远不会运行。如果运行代码,控制台会打印undefined而不是num is not defined错误消息。在这种情况下, num 变量仍然在编译步骤中在变量环境中声明。if 条件仅适用于执行步骤,不适用于编译步骤。

context.jpg

执行上下文

在编译步骤中创建的第一个执行上下文,我们将其称为全局执行上下文(global EC)。对应变量环境中存储的变量就是全局变量。全局执行上下文不同步。运行脚本时可以创建和删除多个执行上下文。

这些执行上下文从哪里来?执行上下文始终链接到编译步骤。多个执行上下文意味着许多编译步骤。该机制与函数相关。

var num = 10

function total() {
	var price = 2
	return num * price
}

var numTotal = total()
console.log(numTotal)

我们知道控制台在执行结束时打印 20,但是执行上下文中发生了什么?与往常一样,它从编译步骤开始,并创建一个全局执行上下文。在变量环境中,我们看到 numnumTotal 变量以及 total 函数。编译结束,开始执行。

num 变量更新为 10,然后 JavaScript 引擎读取 total() 部分。此时,重复相同的两步过程。编译步骤再次开始,但这一次,仅发生在 total 函数中。JavaScript 引擎创建一个新的执行上下文,即 total 执行上下文,并将其堆叠在全局执行上下文之上。

我们称这种结构为栈。堆栈有一个独特的特点:后进先出。这是我们在此机制中唯一关心的功能。与全局环境相同,total 执行上下文中存在一个变量环境,其中有一个未定义的 price 变量。

接下来,在 total 执行上下文中,执行步骤开始。 price变量更新为 2。然后,该函数在 total 执行上下文中查找 price变量,并在全局执行上下文中查找 num 变量。很好,它找到了两个变量,并返回了计算结果。同时,返回值被分配给全局执行上下文中的 numTotal 变量。 total 执行上下文中没有剩余的可执行脚本,因此 JavaScript 引擎将其从堆栈中删除。

现在我们有了堆栈底部的最后一个,即全局执行上下文。唯一剩下的可执行脚本是在控制台中打印变量 numTotal 。这样做之后,整个过程就完成了。我们可以看到JavaScript引擎是如何管理执行上下文的:

  • JavaScript引擎在函数被调用之前不会编译函数中的代码。
  • 编译函数时,会创建一个新的执行上下文并将其放在堆栈顶部。
  • 所有执行上下文都在堆栈结构中进行管理。该过程从堆栈顶部到底部发生。
  • 每当调用函数时,就会发生编译和执行这两个步骤的过程。

根据执行上下文堆叠在彼此顶部的方式,我们将其称为调用堆栈。新的执行上下文会添加到最上面,最上面的执行上下文优先先完成。只有一个全局执行上下文,并且它始终位于调用堆栈的底部。它在浏览器开发工具中显示为“匿名”调用堆栈。

程序生命周期阶段

程序生命周期阶段是计算机程序从初始创建到部署和执行所经历的阶段。这些阶段是编辑时间、编译时间、链接时间、分发时间、安装时间、加载时间和运行时间。生命周期阶段不一定按线性顺序发生,它们可以以各种方式交织在一起。例如,在修改程序时,软件开发人员可能需要在自己的计算机上反复编辑、编译、安装和执行,以确保足够的质量,然后才能分发给用户;然后,用户在其计算机上下载、安装并执行修改后的程序的副本。

  • 编辑时间(edit time)是指正在编辑程序源代码的时间。这涵盖了初始创建到任何错误修复、重构或添加新功能。编辑通常由人执行,但也可以使用自动化设计工具和元编程系统。
  • 编译时间(compile time)是指编译器将源代码翻译成机器代码的时间。其中一部分涉及语言检查,例如确保类型系统的正确使用。成功编译的结果是可执行文件。
  • 链接时间(link time)连接程序的所有必要的机器代码组件,包括外部组件。程序使用外部库实现的函数是很常见的,所有这些函数都必须正确链接在一起。有两种类型的链接。静态链接是指编译器进行连接时,总是在执行之前。然而,动态链接是由操作系统 (OS) 在执行之前甚至执行期间执行的。
  • 分发时间(distribution time)是将程序的副本传送给用户的过程。分发格式通常是可执行文件,但也可以是源代码,特别是对于用解释语言编写的程序。分发方式可以是物理介质,例如 USB 闪存驱动器或通过互联网远程下载。
  • 安装时间(installation time)使分布式程序准备好在用户计算机上执行,这通常包括存储可执行文件以供操作系统将来加载。
  • 加载时间(load time)是指操作系统从存储(例如硬盘驱动器)获取程序的可执行文件并将其放入活动内存中以开始执行的时间。
  • 运行时(run time)是执行阶段,中央处理单元执行程序的机器代码指令。程序可能会无限期地运行。如果执行终止,则可能是正常的预期行为,也可能是崩溃等异常情况。