最近在写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]();
我们可以看到,babel将for循环内的代码单独提取出来了,我们知道闭包可以捕获父级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直接在编译期就做了检查。
总结
let和const在编译后还是以var来声明变量,不同的地方在于,使用let或const声明的变量,如果在上下文环境中存在相同变量名的var,那么会自动将let声明的变量名改成其他名字,简单说就是对变量名在编译期进行重命名。而正是因为这样的重命名的改动,由此引出了很多ES6对于let的其他一些特性,比如:块级作用域、暂时性死区等等。
注意:重命名不是let实现的全部