最近在写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
实现的全部