let
ES6新增了let命令,用于声明变量。其用法类似于var,但是所有声明的变量旨在let
命令所在的代码块内有效。
{
let a = 10;
var a = 10;
}
a; // ReferenceErrorL a is not defined
b; // 1
上述的代码分别用let和var声明了两个变量,然后再代码块之外打印这两个变量,结果let声明的变量报错,var声明的变量返回了正确值。这表明,let声明的变量只在所在的代码块之内有效。所以let很适合再for循环内使用。
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}1000)
}
// 每隔一秒打印i: 0 1 2 3 4
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}1000)
}
// 每隔一秒打印:5 5 5 5 5
console.log(i); // ReferenceError: i is not defined
以上的计数器只在for循环体内有效,在for循环外部引用就会报错。而且在let声明的for循环内部的事件不会有闭包问题,因为let会限定代码块的执行时作用域。
另外,for循环还有一个特别之处,就是设置循环变量的那部分时一个父级作用域,而循环内部是一个单独的子作用域。例如:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i)
}
// 打印 abc abc abc
不存在变量提升
var命令会发生“变量提升”的现象,即变量可以在声明之前使用,值为undefined。这种现象多少有点奇怪,按照一般的逻辑,变量应该在声明之后才能使用。
为了纠正这种现象,let命令改变了语法行为,他所声明的变量一定要在声明后才能使用,否则就会报错。
console.log(foo); // undefined
var foo = 2;
console.log(bar); // Uncaught ReferenceError: Cannot access 'bar' before initialization
let bar = 2;
以上代码中,foo变量用var声明后会发生变量提升,即脚本开始运行时,foo变量已经存在,但是没有值,所以会输出undefined。变量bar用let命令声明则不会发生变量提升。这表明在声明之前,bar变量是不存在的,这时使用它就会报错。
暂时性死区(TDZ)
只要块级作用域内存在let命令,他所声明的变量就“绑定”了这个作用域,不再受外部的影响。
var temp = 123;
if (true) {
temp = 'abc'; // ReferenceError
let temp;
}
上面的代码在全局定义了temp变量,但是在块级作用域中又let声明了一个局部变量,导致后者绑定了这个块级作用域,所以在let声明变量前,对temp赋值会报错。
暂时性死区也意味着typeof不再是一个百分百安全的操作。
typeof x; // Error
let x;
在ES5非严格模式时,typeof一个没有声明的变量会返回undefined
,但是在ES6会直接报错。
有些“四区”比较隐蔽,不太容易被发现。
function bar (x = y, y = 2) {
return [x, y]
}
bar(); // Error
上面的代码中,调用bar函数之所以报错,是因为参数x的默认值等于另一个参数y,而此时y还没有声明,属于“死区”。如果y的默认值是x,就不会报错,因为此时x已声明。
function bar (x = 2, y = x) {
return [x, y]
}
bar(); // [2, 2]
另外,下面的代码也会报错,与var的行为不同。
var x = x; // 不报错
let x = x; // 报错
// ReferenceError:x is not defined
不允许重复声明
let不允许在相同的作用域内重复声明同一个变量。
// 报错
function foo () {
let a = 10;
var a = 1;
}
// 报错
function foo (arg) {
let arg = 1
}
// 不报错
function foo (arg) {
{
let arg = 1
}
}
上面的代码第二个foo之所以会报错,是因为在严格模式下函数的参数默认是用let声明的,可以理解为以下形式。
function foo (let arg) {
let arg = 1 // 报错
}
const
const命令除了具有与let相同的特征:
1. 声明的变量不存在提升
2. 不允许重复声明同一变量
3. 暂时性死区
除此之外,const声明的变量不允许改变
const本质
const实际上保证的并不是变量的值不得改动,而是变量指向的那块内存地址不得改动。
对于简单类型的数据(数字、字符串、布尔值等)而言,值就保存在变量指向的内存地址中,因此等同于常量.
但是对于复杂类型的数据(对象和数组),变量指向的内存地址保存的只是一个指针,const只能保证这个指针是固定的,至于他指向的数据结构是不是固定的,这完全不能控制。因此将一个对象声明为常量时必须小心。
const foo = {};
// 为foo添加了一个属性,可以成功
foo.prop = 123;
// 并且可以改变prop属性
foo.prop = 234;
// 将foo指向另一个对象,指针发生改变,就会报错
foo = {};
如果你真想声明一个不可变的对象,应该使用Object.freeze
方法。
const foo = Object.freeze({});
// 在严格模式下,下面代码会报错;在非严格模式下,下面代码不起作用
foo.prop = 234;
上面的代码中,常量foo指向一个被冻结的对象,所以添加属性时不起作用或报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是将一个对象彻底冻结的函数。
var constantize = obj => {
Object.freeze(obj);
Object.keys(obj).forEach((key, index) => {
if (typeof obj[key] === 'object') {
constantize(obj[key])
}
})
}