let 和 const

let 和 const 是 ES6 新增的用于声明变量和常量的关键字,他们的作用是什么?它们又有什么特性?它们与 var 定义的变量又有何区别?

let 和 const

定义以后可改变的量就是变量,定义后不可改变的量就是常量。

1. 变量和常量

ES6 中,使用 let 命令定义变量,使用 const 命令定义常量,也就是说 let 定义后变量是可修改的,const 定义后的常量不能被修改。这一特性目前被大多数浏览器原生支持,但是针对少部分不能支持的浏览器,我们可以使用 babel 将它编译成 ES5 语法,下面可以看下两者用 babel 编译后的代码有何区别:

// ES6
let a = 1;
const b = 2;
// babel 编译后
"use strict";
var a = 1;
var b = 2;

感觉没区别啊??不要急,再加一段代码:

// ES6
let a = 1;
const b = 2;
b = 3;
// babel 编译后
"use strict";
function _readOnlyError(name) { throw new Error("\"" + name + "\" is read-only"); }
var a = 1;
var b = 2;
b = (_readOnlyError("b"), 3);

这样应该很容易看出区别了,当我们对一个常量进行变值操作,就会抛出一个错误告诉你:这个值是只读的。

b = (_readOnlyError("b"), 3)这种操作我也没见过,不过猜测应该类似于 b = _readOnlyError("b") && 3

注意:由于const 定义的值是不可变的,这点在用 const 定义引用类型的时候要特别注意!如果是引用类型的 const 值,改变其中的属性是可行的,但是通常不建议这么做。

const obj = {};
obj.a = 1;
obj.b = 2;
obj; // => {a: 1, b: 2}
obj = {}; // => 报错

2. 定义就要初始化

由于 const 定义的常量,定义后就不能修改的特性,决定了它定义的时候必须就初始化,否则就报错;而 let 则没有这种限制,它定义的变量完全可以在后面再初始化:

let a;
a = 1;
const b; // Uncaught SyntaxError: Missing initializer in const declaration
b = 2;

上面这段代码由于直接违反了 const 的语法特性,因此在 babel 编译阶段就无法通过

let 和 var

letvar 是两种声明变量的方式,二者主要有以下区别:

1. 作用域不同

  • let 所声明的变量会创建自己的块级作用域,创建的作用域是定义它的块级代码及其中包括的子块中,且无法自动往全局变量 window 上绑定属性。

  • var 定义的变量,作用域为定义它的函数,或者全局,并且是能自动往全局对象 window 上绑定属性的。

能否创建自己的块级作用域这一差别,就会涉及到一道被问烂的面试题:

var result = [];
(function () {
for (var i = 0; i < 5; i++) {
result.push(function () {
console.log(i);
});
}
})();
result.forEach(function (item) {
item()
});
// => 打印出五个 5

为什么打印出了五个5,而不是预期的 0,1,2,3,4 ?因为 var 不会创建自己的作用域,而 js 本身又是没有块级作用域这个概念的,for 循环中定义的变量就等于是直接定义在匿名函数中的变量,于是当这5个函数被掏出来执行的时候,循环早已完成,而函数读取的上面一层作用域中存储的变量 i,也早已经被累加成了5。

然而,这个问题只要将 var 关键字换成 let 就迎刃而解:

var result = [];
(function () {
for (let i = 0; i < 5; i++) {
result.push(function () {
console.log(i);
});
}
})();
result.forEach(function (item) {
item()
}); // => 0,1,2,3,4

我们可以看看这段使用 let 的代码,最后被 babel 转译成什么样:

"use strict";
var result = [];
(function () {
var _loop = function _loop(i) {
result.push(function () {
console.log(i);
});
};
for (var i = 0; i < 5; i++) {
_loop(i);
}
})();
result.forEach(function (item) {
item();
});

从上面的代码我们就可以看出,let 创建作用域的方式,其实就是创建了一个函数,在函数内定义一个同名变量并于外部将这个变量传入其中,以此达到创建作用域的目的。

2. 变量声明提升

  • let 定义的变量不会进行变量声明提升操作,也就是说在访问该变量之前必须要先定义。

  • var 定义的变量存在变量声明提升,因此在变量定义前就能访问到变量,值是 undefined

console.log(a); // undefined
var a = 1;
console.log(b); // Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 2;

同样我们来看下上面这段代码被编译后的样子:

"use strict";
console.log(a);
var a = 1;
console.log(b);
var b = 2;

看起来好像 Babel 无法编译这种阻止变量声明提升的语法,let 声明的变量无法提升的特性应该是浏览器内部的 JS 执行引擎支持和实现的。

3. 暂时性死区

只要块级作用域内存在let / const命令,它所声明的变量 / 常量就“绑定”(binding)这个区域,不再受外部的影响。ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域,凡是在声明之前就使用这些变量,就会报错。这种特性也被成为暂时性死区。

var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError: tmp is not defined
let tmp;
}

同样,这个特性也是被浏览器内部的 JS 执行引擎支持和实现的,babel 无法支持这种特性的编译,只能简单的将 let 编译成 var。但是有意思的是,由于 letif 块中是可以构建自己的独立作用域的,babeltmp 这个变量换了个名字来模拟实现块级作用域的创建:

"use strict";
var tmp = 123;
if (true) {
_tmp = 'abc';
var _tmp;
}

4. 可否重复定义

  • let 定义的变量,一旦定义便不允许被重新定义。

  • var 定义的变量,可以被重新定义。

var a = 1;
var a = 2;
let b = 3;
let b = 4; // Uncaught SyntaxError: Identifier 'b' has already been declared

上面这段代码由于直接违反了 let 定义的变量无法重新定义的语法特性,因此同样在 babel 编译阶段就无法通过。

总结

这块内容并不复杂,我们可以做一个简单的总结:

let 特性:

  • 创建块级作用域。

  • 定义后不能重新定义。

  • 不存在变量提升。

  • 存在暂时性死区。

  • 全局作用域下定义时不会被挂载到顶层对象上(window对象 / global 对象

    // 浏览器环境
    var a = 1;
    window.a; // => 1
    let b = 2;
    window.b; // => undefined

const 特性:

  • let

  • 一旦初始化赋值,后面不能被修改

  • 定义时就必须初始化

相关参考