JavaScript:剖析ES6(1)--let和const

405 阅读5分钟

最近在写GICXMLLayout开源库的时候要支持JavaScript,而在实现的过程中对于ES6的实现原理也有了进一步的了解,因此写几篇博客,已做记录。

注意:

文中出现的结论,只能代表是在使用babel编译的情况下的结论

一、let

先从一个简单的例子开始。

例子1

let a = 1;
console.log(a);

经过babel编译后的代码如下:

var a = 1;
console.log(a);

我们会发现,let在这样的场景下,跟var是没有区别的。

例子2

那如果是这样呢?

{
    let a = 1;
    console.log(a);
}
console.log(a);

经过babel编译后的代码如下:

{
  var _a = 1;
  console.log(_a);//2
}
console.log(a);// ReferenceError

运行上面的代码,对于大括号外面的console.log(a);会直接报ReferenceError错误。之所以会出现这样的情况,是因为babel在编译的时候将let a编译成了var _a,并且将同级作用域内的变量引用一并改为_a,而作用域外的引用没有改变。

例子3 (变量提升)

这个例子是有关let变量提升

console.log(bar);
let bar = 2;

经过babel编译后的代码如下:

console.log(bar);
var bar = 2;

从这里可以看出,let声明的变量,其实还是存在变量提升的问题的。并没有像ES6规范中提到的那样let可以阻止变量提升。然而如果你使用如下代码就又不一样了。

console.log(bar);
{
    let bar = 2;
}

经过babel编译后的代码如下:

console.log(bar);// ReferenceError
{
  var _bar = 2;
}

运行这样的代码你就会得到一个ReferenceError的错误。看起来好像是阻止了变量提升。但我们仔细分析下的话,这完全是因为let在一个块级作用域内定义了,而babel在编译的时候只是将变量名称重命名了而已。

从上面的几个例子也进一步可以分析出,let的所谓块级作用域,简单理解是在同一个作用域内引用的变量名称,在编译的时候被重命名了,而作用域外的变量名不会被重命名,由此引出的结果是,由于变量名被重命名了,因此,对于作用域外的变量名就会报ReferenceError的错误。这也就引出了let块级作用域暂时性死区等一系列特性。

例子4(循环迭代)

这个例子是有关循环的例子。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

如果你把上面的代码中let换成var那么a[6]();输出的将会是10。之所以这样,我们分析下经过babel编译后的代码:

var a = [];
var _loop = function (i) {
  a[i] = function () {
    console.log(i);
  };
};
for (var i = 0; i < 10; i++) {
  _loop(i);
}
a[6]();

我们可以看到,babelfor循环内的代码单独提取出来了,我们知道闭包可以捕获父级function的变量,并且我们也知道对于number这样的基本数据类型,JS在传参的时候是直接拷贝的,而不是引用。因此对于_loop这个方法,每次传过来的i都会被拷贝一份,而闭包捕获的变量仅仅是一个已经被拷贝的变量而已,也即是变量的地址已经改变,不是for循环中i的地址了。在没有let的时候,要解决这样的问题,我们采用的方法往往也是使用闭包(立即执行函数)来实现。

例子5

var tmp = 123;
if (true) {
  {
    tmp = 'abc'; 
  }
  let tmp;
}

经过babel编译后的代码如下:

var tmp = 123;
if (true) {
  {
    _tmp = 'abc';//ReferenceError
  }
  var _tmp;
}

运行上面的例子你会得到一个ReferenceError的错误。这个例子充分说明了let关键字的块级作用域的功能,只要是在同级作用域内,所有引用了相同变量名的地方都会被编译成新的变量名

我们可以得出一个结论。

let的块级作用域的本质就是通过babel重命名变量

例子6

  let a = 10;
  var a = 1;

这样的代码,你连编译都无法编译,ES6规定在同级作用域内不允许存在相同的变量名,因此babel直接在编译期就报错了。

二、const

const的原理跟let其实差不多。但是多了一个不可重复赋值。

例子1

const PI = 3.1415;
PI = 10;

经过babel编译后的代码如下:

function _readOnlyError(name) { throw new Error("\"" + name + "\" is read-only"); }

var PI = 3.1415;
PI = (_readOnlyError("PI"), 10);

我们可以从编译后的代码中看到,当我们试图对一个const的变量赋值的时候,babel直接将赋值代码替换成直接调用_readOnlyError方法来抛出异常。

例子2

const必须在声明变量的时候就赋值。如果我们不赋值呢?比如下面

const PI;

你会发现,无法完成编译。babel直接在编译期就做了检查。

总结

letconst在编译后还是以var来声明变量,不同的地方在于,使用letconst声明的变量,如果在上下文环境中存在相同变量名的var,那么会自动将let声明的变量名改成其他名字,简单说就是对变量名在编译期进行重命名。而正是因为这样的重命名的改动,由此引出了很多ES6对于let的其他一些特性,比如:块级作用域暂时性死区等等。

注意:重命名不是let实现的全部