1. 简介
在ES6以前,变量的声明都是使用var关键字,且会进行变量声明提升。另外,我们曾经讲过,JS中是没有块级作用域的,这一点也带来了很多的不便。ES6 新增了let和var两个关键字,用来声明变量。下面我们就来看看他们的用法。
2. let
我们来看下面一段代码。
function f() {
var a = 1;
}
{
var b = 2;
}
// console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // 2
当变量a在函数f内使用var声明时,在全局无法直接引用该变量。但是在全局函数内用一对花括号包裹的区域中生命的变量b,却可以在全局中直接引用。因为对于JS来讲,是没有块作用域的。这一点和JAVA等语言有着很大的不同,也带来了很多不便。举一个简单的例子:
var i = 1;
... // 一堆其他的操作
for (var i = 0;i<3;i++) {
console.log(i); // 0 1 2
}
console.log(i); // 3
在for循环内部生命的变量i其实是无效的,因为在同个作用域(此处是全局作用域)已经声明过变量i。此时var i = 3;中的声明会被忽略,而只保留i =3;(这块内容可以参考我的文章JS入门难点解析3-作用域)。所以,在for循环结束以后,你满心以为会输出最开始在全局声明赋值的var i = 1;时,结果却是被循环改变的结果3。这明显不是我们希望的结果,那么js中能否也使用块级作用域呢,我们生命的变量可否只在块级作用域中生效呢?ES6给我们提供了let。看下面代码:
let i = 1;
... // 一堆其他的操作
for (let i = 0;i<3;i++) {
console.log(i); // 0 1 2
}
console.log(i); // 1
将for循环中的var改成let,其声明的变量i就只在循环内生效了。是不是更加灵活方便了呢?当然,let使用时有些需要注意的地方。
2.1 不存在变量提升
var命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined。(可以参考我的文章 JS入门难点解析2-JS的变量提升和函数提升)这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 2
var foo = 2;
// let 的情况
console.log(bar); // Uncaught ReferenceError: bar is not defined
let bar = 2;
2.2 暂时性死区
我们之前讲到过,let没有变量提升,也就是说在let声明一个变量之前对其引用会报错。这个很好理解。但如果此时该变量在块作用域外部也被声明了呢?是否此时的引用是对外部该变量的引用呢?
看下面这段代码:
var tmp = 123;
if (true) {
tmp = 'abc'; // Uncaught ReferenceError: tmp is not defined
let tmp;
}
这里,tmp = 'abc';一句会报错。也就是说,let不仅不允许其声明的变量在其声明前被引用,还不允许其引用外部的同名变量,相当地霸道。其实,ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
需要注意,TDZ有时会导致代码出错。比如:
曾经安全的typeof可能不在安全:
typeof y; // 未被let锁定,输出undefined
typeof x; // 被let锁定,报ReferenceError
let x;
另外,有些TDZ导致的错误会十分隐晦:
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
改成
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
就okay了。还有如下情况:
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
2.3 不允许重复声明
let不允许在相同作用域内(指的是ES5中规定的作用域,不包含块级作用域)重复声明一个变量,不管是使用var还是let。
var a = 1;
let a = 2;
console.log(a); // Uncaught SyntaxError: Identifier 'a' has already been declared
以及
let b = 2;
var b = 1;
console.log(b); // Uncaught SyntaxError: Identifier 'b' has already been declared
还有
let c = 1;
let c = 2;
console.log(c); // Uncaught SyntaxError: Identifier 'c' has already been declared
另外看下边两组代码。
let a = 1;
if (true) {
var a = 2;
console.log(a); // Uncaught SyntaxError: Identifier 'a' has already been declared
}
let a = 1;
function f(){
var a = 2;
console.log(a); // 2
}
f();
还有一点,需要注意:
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
3. const
const的作用很简单。const声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
在代码中,我们将长会将一些常量用一些有实际意义的名称去命名。比如上面代码段中的圆周率PI。
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。对于const来说,只声明不赋值,就会报错。
const foo;
// SyntaxError: Missing initializer in const declaration
const的作用域与let命令相同:只在声明所在的块级作用域内有效,其声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
需要注意的是,const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
如下:
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。
如果真的想将对象冻结,应该使用Object.freeze方法。(可以参考我的文章 JS入门难点解析13-属性描述符,数据属性和访问器属性)
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
参考
let 和 const 命令
深入ES6 (二)let和const
ES6这些就够了