"var a=1;" 在JS中到底发生了什么?

一个非常基本的问题,但是想要回答它,却需要先了解一些关于 JavaScript 引擎的知识。

JavaScript 引擎介绍

什么是 JavaScript 引擎?简单来讲,就是能够将 JavaScript 代码处理并执行的运行环境。通俗得讲,当你写了 var a = 1 + 1; 这样一段代码,JavaScript 引擎做的事情就是看懂(解析)你这段代码,并且将 a 的值变为 2。不过,要真的解释 JavaScript 引擎,我们需要了解一些编译器的基本概念以及现代语言需要的一些新编译技术。

JavaScript 属于什么语言?编译型or解释型?

如何看待编译型语言与解释型语言的区别呢?要回答这个问题,粗俗地打个比方吧:餐馆里面就餐的时候,编译型就是一次性将所有菜都已经提前在厨房里做好了,一口气给你端上来;而解释型就是客户在就餐的时候,厨房现炒现做。

我们先来看看典型的编译型语言:C/C++。为了让机器明白你所写代码的含义,该语言通常的做法是使用编译器将源代码编译成本地代码。这一过程叫编译过程,在代码执行之前就先编译好。操作系统调度 CPU 直接执行这些编译好的本地代码,无需额外的操作。这个发生在代码执行前的完整编译过程叫做 AOT(Ahead of Time),即:提前编译。

编译过程

典型的解释型语言是 Python 等脚本语言。处理脚本语言的通常做法是开发者将写好的写好的代码直接发给用户,用户使用脚本的解释器将脚本加载然后解释执行。通常情况下,脚本语言不需要开发人员去编译脚本代码。

解释过程

再来看看 Java 语言。它属于一个变种,可理解它为混合型。为了消除 C/C++ 对不同操作系统之间不友好的缺陷,Java 提出了“Write once,Run everywhere”的口号,实现了与具体操作系统无关的编译过程。它的做法分为两个阶段:

  • 首先是像 C++ 语言一样的编译阶段。但这个阶段它生成的并不是本地代码,经过编译器生成的是字节码。字节码是跨平台的一种中间表示,它与平台无关,能够在不同操作系统上运行。
  • 运行字节码阶段。Java 的运行环境是 Java 虚拟机加载字节码,使用解释器来执行这些字节码。但仅仅是这样,肯定比C++语言慢不少。于是现代 Java 虚拟机都会使用 JIT(Just in Time) 技术来提升效率,即:即时编译。JIT 技术通过将字节码转变为本地代码来提升效率。

Java虚拟机的过程

最后回到 JavaScript 语言。在该语言设计之初,它的主要目的是为了处理以前由服务器负责的一些输入验证操作,是一个完完全全的解释性脚本语言,只需要使用解释器来解释即可。但随着互联网与浏览器的兴起,它能够完成的功能也越来越强大。工程师们投入了资源来提高它的速度,通过借鉴 Java 虚拟机和 C++ 编译器中的众多技术来改进其工作方式。其中一个关键的技术就是 Java 虚拟机中的 JIT 技术,即将抽象语法树转成中间表示,然后通过 JIT 技术转成本地代码,这能够大大提高执行效率。

乍看 JavaScript 与 Java 的编译过程挺相似的。但实际上,两者之间的区别如下:

  1. 类型。JavaScript 是无类型的语言(也被称为动态语言),其对于对象的表示和属性的访问都比 Java 存在更大的性能损失。
  2. JIT 阶段的时间点。Java 语言通常是先将源代码编译成字节码,然后再在执行阶段执行,即编译阶段与执行阶段是分开的,也就是说从源代码到抽象树再到字节码这段时间的长短是无所谓的,只需要尽可能地生成高效的字节码即可。但对于 JavaScript 而言,JIT 的过程是在 JavaScript 源文件下载后的网页加载执行阶段共同实施的,所以这个过程的时间处理会有很高的要求。

但是实际上,JavaScript 引擎的执行步骤非常复杂。对于 JavaScript 来说,大部分的编译工作都发生在代码执行前的几微妙(甚至更短!)。所以每个阶段的时间越少越好。不同的 JS 引擎都在不同的步骤中有各自独特的优化工作。在此不详细展开。

JavaScript的编译执行过程

于是,从这个角度,我们可以尝试回答关于 JavaScript 属于什么语言的问题。在该语言设计之初,它是实实在在的解释型语言。在工程师们尝试引进更多编译型语言相关的技术(如JIT、字节码等) 的时候,它可以被认定为是一种编译型语言。

JavaScript引擎到底做了啥?

所以回到我们题目的那个问题:"var a=1;" 在JS中到底发生了什么?

你可能会认为这是一句声明。但是我们的引擎却不这么看。事实上,引擎认为这里有两个完全不同的声明,一个由编译器在编译时候处理,另一个则由引擎在运行时处理。

可以看到,此时我们再去看 JavaScript 引擎的时候,可以将其分为两个阶段:编译阶段、执行阶段。

我们明确创造两个角色:引擎、编译器,来详细回答文章标题的提问。如下:

遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的及合作。如果是,那么编译器会忽略该声明,继续进行编译;否则他会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a 。 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a=1这个赋值操作。引擎运行时候会首先询问作用域,在当前的作用域中是否存在一个叫做a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(通过作用域链)。如果引擎最终找到了a变量,那么就会将 1 赋值给它。否则引擎就会抛出一个异常!