萝三画室

深入理解JS-part1-编译和变量提升

本系列深入理解JS是对JavaScript这门语言中一些比较特殊和难懂的点做的说明和总结。JavaScript被认为是一门简单易用的语音,它具有许多复杂的概念和原理,但却魔术般的使用一种看起来很简单的方法体现出来,这也使得很多开发者仅仅停留在能够使用的层面上,而未曾真正的深入理解其中的原理。这样的知其然而不知其所以然,会导致在实际应用中JS表现出不曾预料到的诡异行为,然后很多人会避开他们,去采用其他“安全”的方法。这显然不是正确的途径。本系列旨在弄清“为什么”,从书中或实际应用中遇到的困惑出发,探究出现“诡异行为”的原因,深入理解整个JavaScript。

本文是深入理解JS系列的part1,从编译JS出发,直到了解什么是变量提升。

参考书籍:《你不知道的JavaScript》

编译语言JavaScript

大多数情况下,我们都会把JS描述为直译型解释型 语言。再加上它可以直接在浏览器中运行而无需像C++等语言一样通过编译器编译,导致很多人对JS产生一种误解(以前我也是这样!)– JavaScript无需编译。这是大错特错的!带着这种误解,我们的直觉就会告诉我们,JS在运行时是从上至下顺序执行代码的,这样也就很难理解为什么会出现变量提升。所以,要深入理解变量提升,我们首先要牢牢的记住:JavaScript是一门编译语言

JS编译原理

我们已经知道,JS是一门编译语言了。也就是说,JS代码在运行之前需要编译,并非是所见即所得。然而,JS的编译原理和过程与传统编译语言大不相同。

  • 传统编译语言如C++,是提前编译 的,可以简单理解为我们在启动一个exe时,访问的是编译后的程序(编译->访问->运行)。我们编写代码,编译生成编译结果,并可以将编译结果再分布式系统中移植。
  • JS,是运行前编译 的,可以简单理解为我们在浏览器中键入一个地址时,访问的是未经编译的程序(访问->编译->运行)。我们编写代码,浏览器访问,进入到一个作用域后进行编译,编译后即刻运行。

JS编译过程

JS编译过程中,涉及两个协作的“好朋友”。负责编译和运行JS代码的JavaScript引擎,负责协助JavaScript引擎查找变量的作用域
下面我们看看JS的编译过程。编译过程实际上就是找到所有的声明,并通过作用域将其关联起来。
程序运行过程中,进入到某个作用域->引擎处理代码进行编译->遇到变量,询问作用域,由作用域进行变量查找(可能的结果包括变量未声明报错、新变量声明、取得变量源值)->作用域中所有代码编译完毕->引擎执行编译后的代码。

变量提升

对于在作用域中使用var声明的变量以及定义的function,会出现变量提升 的现象–在变量声明之间就可以使用变量。

1
2
3
4
function init(){
console.log(a);
var a = 12;
}

上面的函数执行后,控制台会输出undefined(变量声明但未赋值)而不是抛出ReferenceError错误(变量未声明)。

如果是按照前面讲过的错误观念:”JS在运行时是从上至下顺序执行代码的”这种理解,那么在console.log时还不存在变量a的,应该抛出ReferenceError错误。

现在按照我们在JS编译过程 中将的步骤,再来看看实际上发生了什么。

对于上面的例子,当编译console.log(a)时,JS引擎询问了init函数的作用域是否存在变量a。用域查找发现自身中存在var a的声明,因此不会出错,仅仅是变量未赋值。

有人可能会问,既然能发现var a的声明,为什么不能识别a的赋值呢?这是因为,在编译阶段,引擎并不执行代码,赋值运算实际是运行阶段的任务,因此作用域在找到var a的声明后,就不会继续进行a = 12这步赋值运算了。

于是前面的代码就可以理解为编译之前的状态,而经过javascript引擎编译后直译的状态是:

1
2
3
4
5
function init(){
var a ;
console.log(a);
a = 12;
}

这也就是会出现变量提升 的原因了。

关于变量提升,还有以下需要注意的地方:

  • var 声明的变量以及函数会出现变量提升,函数的优先级高于var
  • ES6中,let、const 声明的变量不存在变量提升