前言
记录学习,知识沉淀。理解 JavaScript ES6 新的定义变量的方式
let/const
let/const 是 ES6 中提出的新的用于定义变量的方式,与 var 相比具有以下特性
全局作用域下声明的变量不会成为 window 对象的属性/方法
// 全局作用域下 var 声明的变量会自动成为 window 对象的属性/方法
var my_github = 'github.com/yuanyxh';
console.log(window.my_github); // github.com/yuanyxh
// 全局作用域下 let/const 声明的变量不会成为 window 对象的属性/方法
let my_name = 'yuanyxh';
console.log(window.my_name); // undefined
const MY_BLOG = 'yuanyxh.com';
console.log(window.MY_BLOG); // undefined
块作用域
let/const 具有块作用域,即变量作用域被限制在当前代码块,这为什么是重要的,我们看一个经典示例
// var
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // 5 5 5 5 5
}, 0);
}
// let
for (let j = 0; j < 5; j++) {
setTimeout(function () {
console.log(j); // 0 1 2 3 4
}, 0);
}
可以看到,使用 let 定义的变量输出似乎符合预期效果,使用 var 定义的变量则出了问题,为什么?
JS 引擎在内部会为每次循环声明新的迭代变量,每个循环引用的都是不同的变量实例, 但因为 var 定义的变量没有块作用域,变量渗透到了循环体外,每个 setTimeout 引用的都是同一个循环体外的变量 i,i 最后一次的值是 5,不满足循环要求退出了循环,所以此时循环体外的 i 值为 5,执行顺序大致如下
// 第一次循环 循环体内外 i 值为 0
// 添加一个异步任务
// 第二次循环 循环体内外 i 值为 1
// 添加一个异步任务
// 第三次循环 循环体内外 i 值为 2
// 添加一个异步任务
// 第四次循环 循环体内外 i 值为 3
// 添加一个异步任务
// 第五次循环 循环体内外 i 值为 4
// 添加一个异步任务
// 循环结束 循环体内外 i 值为 5
// 消耗异步任务队列 输出 i
为考虑浏览器兼容性,很多编译转换工具都会将 let/const 声明编译为 var 声明,那怎么解决上述问题呢,如下
// IIFE and closure
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 0);
})(i)
}
// or
for (var i = 0; i < 5; i++) {
try {
throw i;
} catch (num) {
setTimeout(function () {
console.log(num);
}, 0);
}
}
第一处代码利用了函数 立即调用表达式(IIFE),将迭代变量传递给了 IIFE 的形参 j,var 是有函数作用域的,所以每个 setTimeout 引用的都是不同的变量实例。
第二处代码利用了 try ... catch 中的 catch 块具有块作用域的特点实现了相同效果。
暂时性死区
var 有声明提升1,而 let/const 声明的变量不允许在词法声明前使用,即 暂时性死区2,如下
// var
// 允许,但强烈不建议
console.log(j); // undefined
var j = 0;
// let
// 不允许
console.log(i) // ReferenceError
let i = 0;
可以看到 j 的声明被提升到了顶部,但赋值操作未被提升,所以打印结果为 undefined,相当于
var j;
console.log(j); // undefined
j = 0;
let/const 声明的变量本质上也存在声明提升,但 JS 引擎对其进行了词法检查,不允许 let/const 定义的变量在词法声明前使用。
同一作用域不允许重复声明
let/const 声明的变量在同一作用域内不允许重复声明
// var
// 允许,但不推荐
var i = 0;
var i = 10;
// let
// 不允许
let j = 0;
let j = 10; // SyntaxError
// compose
// 不允许
var k = 0;
let k = 10; // SyntaxError
另外,var 声明的变量,在同一作用域内重复声明且后续声明未赋值时,后面的变量声明会使用前面变量声明的值
// var
var i = 10;
var i;
console.log(i); // 10
let 与 const 的区别
let定义的变量,变量值允许改变const定义的变量,变量值不允许改变,且声明变量的同时必须进行初始化
// let
let i = 0;
i = 10;
// const
const TEST = 0;
TEST = 10; // TypeError
const 定义的变量不能改变的是变量对数据的引用,但数据本身是可以改变的
// const
const obj = { name: 'yuanyxh', blog: 'yuanyxh.com' };
obj.name = 'jack';
console.log(obj); // { name: 'jack', blog: 'yuanyxh.com' }
最佳实践
以下给出三种变量声明方式的最佳实践,摘自阅读的书籍与个人理解
- 优先使用
let/const - 对于始终不变的数据优先使用
const - 对于尚未确定是否会改变的数据优先使用
const,以后改变数据时代码编辑工具会报语法错误 - 预先声明所有将要使用的变量
参考资料
《JavaScript 高级程序设计》
《你不知道的 JavaScript》
ECMAScript 规范
ECMAScript 特性兼容表
ECMAScript 规范部分译文