前言
我们都知道在 ES6 标准中提出了两个定义变量的新关键字 let 和 const, 那么如果需要将 ES6 的代码转化为 ES5,如何解决 let 和 const 的语法问题,当然你可以选择用 babel 的 polyfill,但是用过 babel 的同学可能都发现了,babel 只是简单的将 let 和 const 转化成了 var,那么如果我们想要保留一些 let 和 const 的特性又该如何做呢?如何在 ES5 环境下模拟实现 ES6 的 let、const 关键字呢?让我们来尝试着解决下
首先来看下关于 let、const 的定义标准
- let 声明一个块级作用域的局部变量,const 声明一个块级作用域的只读常量
- let 和 const 声明的标识符必须是一个合法的 javascript 标识符
- let 和 const 声明的标识符不允许被重新声明
- let 和 const 声明的标识符未初始化前存在暂时性死区
- const 声明的标识符不允许被重新赋值
以上就是 let 和 const 的定义标准,可以看出相比于使用 var 来声明变量,let 和 const 增加了更严谨的约束,这也是为了解决使用 var 来声明变量留下的各种问题,同时也可以看出 javascript 有正在慢慢从弱类型向强类似语言靠近的趋势
接下来我们看看如何实现这个需求
首先我们知道 javascript 中声明的变量都是保存在作用域中,那么我们可以创建一个对象来模拟 javascript 中的作用域,把声明的变量看作是对象的属性
var __scope__ = {};
第一个问题,let 和 const 声明的标识符必须是一个合法的 javascript 标识符,由于对象的属性名都是字符串类型,因此所有的变量名都可以作为对象的属性名,当然我们也可以做一层校验,判断变量名中是否存在不合法的字符,最简单的方法就是使用正则判断,具体实现这里就不写了
不允许重复声明和作用域提升
由于 javascript 中有预编译的概念,声明变量会被提升到作用域的顶层,而 let 和 const 声明的标识符不允许被重新声明,javascript 在预编译阶段就能检测出重复声明的标识符,因此我们也要做一层检查
我们利用一个数组先来保存所有声明的标识符,类似于预编译阶段的变量声明提升:
var __scope__ = {};
var __variable__ = [];
for (var i = 0; i < __variable__.length; i++) {
var property = __variable__[i];
if (__scope__.hasOwnProperty(property)) {
throw new SyntaxError(
"Identifier " + property + " has already been declared"
);
}
}
然后利用对象的 hasOwnProperty 方法来检查__scope__是否拥有重复的属性,如果__variable__数组中有重复的 key 就会抛出一个重复声明的错误,如此我们就解决了变量重复声明的问题
关于暂时性死区
在 ES6 中,let 和 const 声明会被提升但未初始化,在变量声明之前引用块中的变量会导致错误,因为从块开始到处理声明为止,该变量都处于“临时死区”中
ok,我们接下来解决在声明之前访问变量的问题,这一步我们也可以在收集变量阶段一起处理,利用 ES5 的 Object.defineProperty API 对访问进行劫持:
var __scope__ = {};
var __variable__ = [];
for (var i = 0; i < __variable__.length; i++) {
var property = __variable__[i];
if (__scope__.hasOwnProperty(property)) {
throw new SyntaxError(
"Identifier " + property + " has already been declared"
);
} else {
Object.defineProperty(__scope__, property, {
configurable: true,
enumerable: false,
get() {
throw new ReferenceError(
"Cannot access " + property + " before initialization"
);
},
});
}
}
如果在变量初始化前访问,就会抛出一个引用错误,这样我们就解决了变量初始化前引用的问题
let 和 const 的实现
接下去我们看看如何实现 let 和 const 声明变量
我们来实现一个 let 函数来模拟:
function __let__(key, value) {
return Object.defineProperty(__scope__, key, {
configurable: false,
enumerable: false,
get: function () {
return value;
},
set: function (newValue) {
value = newValue;
},
});
}
key 值为 let 声明的变量名,value 值为赋给变量的值,由于 let 声明的变量可以被重新赋值,因此同样做出了改变值处理,这样我们就可以在语法分析阶段做类似如下处理:
let js = "jsvascript";
// 转义后
__let__("js", "jsvascript");
实现了 let,我们再来看看 const,const 声明的变量有一项要求,需要在初始化时赋值,因此我们调整一下:
function __const__(key, value) {
if (arguments.length === 1) {
throw new SyntaxError("Missing initializer in const declaration");
}
return Object.defineProperty(__scope__, key, {
configurable: false,
enumerable: false,
get: function () {
return value;
},
set: function () {
throw new TypeError("Assignment to constant variable");
},
});
}
我们利用函数的 arguments 长度来判断是否给了初始值,注意这里不能使用 undefined 来判断,因为 const 允许初始化值时使用 undefined,如果没有传递第二个参数,那么就会抛出一个错误:
const immutable = undefined
const immutable;
immutable = 'immutable'
// 转义后
__const__('immutable', undefined) // √
__const__('immutable') // ✖ Uncaught SyntaxError: Missing initializer in const declaration
__scope__.immutable = 'immutable' // ✖ Uncaught TypeError: Assignment to constant variable
const 声明的变量不允许被重新赋值,我们在 set 方法中对做了拦截处理,如果变量被重新赋值就会触发 set 方法,然后抛出一个错误,当然这里也可以通过 设置 writable 属性为 false 加上严格模式来实现
关于块级作用域
let 和 const 声明的变量具有块级作用域的特点,这个不难实现,我们可以用一个闭包来解决:
{
let js = "jsvascript";
}
// 转义后
{
(() => {
__let__("js", "jsvascript");
})();
}
关于作用域链
在 Javacript 中存在作用域的概念,内层可以访问外层的作用域,全局作用域作为最顶层的作用域,可以被整个程序访问,这种结构和原型链的概念非常相似,因此我们可以利用原型链来实现这种效果:
(function (__globalScope__) {
var __scope__ = {};
Object.setPrototypeOf(__scope__, __globalScope__);
})(__scope__);
在利用闭包隔绝作用域的时候,可以把外层的作用域传递进去当做内层作用域的原型,这样顺着原型链就能找到外层作用域上声明的变量了
写在最后
以上就是关于如何在 ES5 中模拟实现 let 和 const 的效果,当然这其中还有很多不足,而且 ES6 的 let 和 const 是由底层实现的,再怎么样模拟也无法达到一模一样的效果,本文只是提供了一种思路,在实现过程中可以加深对 let 和 const 的理解,这才是思考这道题的最终目的