引言

  JavaScript在ES6之后引入了两个很重要的关键字:letconst。在学习ES6的时候,我们可能会看到某些资料上写着let不存在变量提升。
但是最近在逛掘金的时候,却发现有人说let是存在变量提升的。

  那么这个就引起了我的兴趣,之前学习的时候跟着教程/课本,理所当然的接受了书上的知识,现在有人提出了质疑。我也就去看看“事实”是什么。

JS编译过程

  其实谈到变量提升,或许应该是从JS编译讲起。JS编译肯定是在代码执行前发生的,一般说来,JS的编译经过三个阶段(当然编译是一个很复杂的过程,下面的阶段只是一个简化的描述)。

  第一个阶段是编译器将程序分解为词法单元(有意义的代码块),第二个阶段是将上一步词法单元流数组解析转换成由元素层级逐级嵌套所组成的代表程序语法解构的树,抽象语法树(AST)。这个很有意思的,大家可以通过这个网站,在线解析一些js代码到AST。

  第三阶段是代码的生成,就是将AST转换为可执行的代码,即一组机器指令。到这个阶段JS代码就可以执行了。

词法作用域

  JavaScript采用的是词法作用域。什么叫词法作用域呢?我们知道js编译的第一个阶段是分词,词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的。词法分析器去解析代码的时候会保持作用域不变。

关系

function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log( a, b, c );
    }
    bar(b * 3);
}
foo( 2 ); // 2 4 12

  无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定,从上面这个代码来理解一下作用域。整块代码就是全局作用域,这个作用域只包含一个标识符:foo,接下来看函数foo,包含形参a在内,被foo函数大括号包围的就是嵌套在全局作用域下的一个子作用域:foo作用域,这个作用域包含abbar三个标识符。然后函数bar,包含形参c在内,嵌套在foo作用域里的bar作用域,标识符只有c。

查找

  js引擎通过如上的结构位置关系去查找标识符的位置。代码中console.log(a, b, c)在执行时,引擎查找a、b、c的声明,从最内层的作用域开始查找,找到了c,但是a没有找到。引擎会从上一级嵌套的foo作用域查找,找到了a,于是引擎使用了这个引用。

对变量的查找工作还可以继续详细的了解:

LHS和RHS查询

var a = 0;,这个JS引擎会分几步完成呢?JS会将他看成两句声明:var a;a = 0;。第一个定义声明在编译阶段进行,第二个赋值声明留在原地等待执行阶段。

变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在引用域中查找该变量,如果能够找到就会对它赋值。

  LHS和RHS就是指上面说的对变量的两种查找操作,查找的过程是由作用域(词法作用域)进行协助,在编译的第二步中执行。LHS(Left-hand Side)引用和RHS(Right-hand Side)引用。通常是指等号(赋值运算)的左右边的引用。

console.log(a)

  这里的对a的引用是RHS引用,这里a没有赋值操作,只是想查找并取得a的值

a = 2;

  这里对a的引用是LHS引用,因为这里我们想要为这个赋值操作找到对应的目标。

  (对于上文的代码中)嵌套作用域中,引擎首先在bar函数作用域中查找a,并尝试对它进行RHS引用,没找到,接着在上一级作用域中查找a...

遮蔽

  如上所说,JS引擎从运行最内部开始逐层向外进行,直到遇到第一个匹配的标识符为止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”,内部的标识符“遮蔽”了外部的标识符。

var a = 0;
function test(){
    var a = 1;
    console.log(a);//1
}
test();

  全局变量会自动为全局对象的属性,因此可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量如果被遮蔽了,无论如何都无法被访问到。

var a = 0;
function test(){
    var a = 1;
    console.log(window.a);//0
}
test();

异常

  当对一个变量进行RHS查询没在作用域中找不到所需变量时候,就会抛出ReferenceError,若变量没有定义,就会抛出错误。

  与RHS不一样,若LHS时候变量未定义,会在全局作用域中创建该变量(前提是运行在非严格模式下)。

a = 2;
consol.log(window.a);

  如果RHS查询到变量,但是你尝试对这个变量进行不合理的操作,例如调用一个非函数类型,查询 null 和 undefined 类型值的某个属性,引擎就会抛出 TypeError

var a = 1
a() // Uncaught TypeError: a is not a function
console.log(a.info.name); // Uncaught TypeError: Cannot read property 'name' of undefined

  在函数调用时候我们,传入参数其实隐藏了一个LHS和RHS。

function foo(a) {}
foo(b)

foo在调用时实际上相当于在函数foo中执行了一次var a = b,这里包含了一个RHS和LHS,若b没有定义的话就会抛出ReferenceError

动态作用域

  动态作用域它并不关心函数和作用域是如何声明以及在何处声明,它只关心从何处调用,换句话说,作用域是基于栈,而不是代码中的作用域嵌套。
总而言之,词法作用域是在定义时确定的,动态作用域是在运行时确定的。

let存在变量提升么

了解了上面的一些知识,我回到最初的问题,let是否存在变量提升呢?由于篇幅原因,我将会把这篇文件分为上下两篇。明天再写吧,今天有点晚了。

(未完待续...)

参考资料

Last modification:January 26, 2022
如果觉得我的文章对你有用,请随意赞赏