一. let
let和var一样用于定义变量,和var的区别在于:let是块级的,而var是函数级的。在以前的JavaScript中,只有两种作用域,一种是全局作用域,另一种是函数作用域,ES6通过let关键字增加了块级作用域。
(1)let的块级作用域
{
var
a
=
1
;
let
b
=
2
;
}
console
.
log
(
a
,
b
);
此时将会报错:Uncaught ReferenceError: b is not defined,因为变量b无法在块级作用域以外被访问。
{
var
a
=
1
;
let
b
=
2
;
{
console
.
log
(
a
,
b
);
}
}
此时输出:1 2 。子级是可以访问到父块级作用域中的变量。
for
(
var
i
=
0
;
i
<
5
;
i
++) {}
console
.
log
(
i
);
此时输出:5 。若将var改为let,也会报错:Uncaught ReferenceError: i is not defined。因为let声明的i只在for循环中起作用。
(2)let不存在变量的提升
function
f
() {
console
.
log
(
a
);
var
a
=
1
;
}
f
();
定义一个函数f()并执行,结果:undefined,此时并没有报错,因为var声明有变量提升的作用,在var声明的变量a之前使用到a,因为此时a还未赋值,所以值为undefined的。以上的代码相当于如下:
function
f
() {
var
a
;
console
.
log
(
a
);
a
=
1
;
}
f
();
若将var改为let:
function
f
() {
console
.
log
(
a
);
let
a
=
1
;
}
f
();
则报错:Uncaught ReferenceError: a is not defined,说明let不能使变量的声明提升。
变量临时失效现象。如下:
var
a
=
1
;
function
f
() {
console
.
log
(
a
);
}
f
();
此时f()函数执行时肯定是可以访问到a变量,结果:1。若代码为如下:
var
a
=
1
;
function
f
() {
console
.
log
(
a
);
let
a
=
1
;
}
f
();
此时将报错:Uncaught ReferenceError: a is not defined 。因为在f()函数的块级作用域内,此时已经有let定义的变量a,所以外面var定义的全局变量a将失效,而console.log是在声明变量a之前调用,并且let声明没有变量提升的功能,所以此时a还未定义,所以会报错。ES6明确规定,如果在区块中存在let和const命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域,外面的同名变量不起作用,并且只要在声明之前使用这些变量,就会报错,这在语法上称为“暂时性死区”(Temporal Dead Zone,简称TDZ),如下所示:
var
a
=
1
;
function
f
() {
//TDZ开始
a
=
2
;
//Uncaught ReferenceError: a is not defined
console
.
log
(
a
);
//Uncaught ReferenceError: a is not defined
let
a
=
1
;
//TDZ结束
console
.
log
(
a
);
//1
}
f
();
TDZ的存在以及变量不可提升的原因是为了减少运行时错误,防止变量在声明前就使用。
(3)let不允许重复声明变量
{
var
a
=
1
;
var
a
=
2
;
console
.
log
(
a
);
}
用var声明两次变量a,并且赋予不同的值,结果输出为:2 。说明了var声明变量允许重复声明,并且值为最后一次赋予的值。若将var改为let:
{
let
a
=
1
;
let
a
=
2
;
console
.
log
(
a
);
}
{
let
a
=
1
;
var
a
=
2
;
console
.
log
(
a
);
}
以上两个都报错:Uncaught SyntaxError: Identifier 'a' has already been declared 。说明let不允许在相同作用域中重复声明变量。不过以下这种使用没有影响:
{
//作用域1开始
let
a
=
1
;
console
.
log
(
a
);
//1
//作用域1结束
{
//作用域2开始
let
a
=
2
;
console
.
log
(
a
);
//2
}
//作用域2结束
//作用域1开始
console
.
log
(
a
);
//1
}
////作用域1结束
从以上可以看出,let的这种块级作用域以及不允许重复声明的特性,可以用来替代立即执行匿名函数的作用,如下:
var
config
= (
function
(){
var
config
= [];
config
.
push
(
1
);
config
.
push
(
2
);
config
.
push
(
3
);
})();
可以使用let的写法来替代:
let
config
;
{
config
= [];
config
.
push
(
1
);
config
.
push
(
2
);
config
.
push
(
3
);
}
(4)实际的例子
var
arr
= [];
function
f
() {
for
(
var
i
=
0
;
i
<
5
;
i
++){
arr
.
push
(
function
() {
console
.
log
(
i
);
})
}
}
f
();
arr
[
2
]();
以上代码想实现的功能是:arr数组中的每个元素保存一个函数,调用指定下标的函数时能输出对应的下标。然而并不像预想的那样会输出2,结果输出为5,arr[1]()....,arr[4]()的输出结果都是5。因为for循环中var定义的i的作用域为整个f()函数,每一次循环,新的i值都会覆盖旧值。执行完函数f()后,i最终为5,而arr中函数使用到的始终是这个i,所以最终执行都输出为5 。
要想实现预想的功能,以前的做法是通过立即执行匿名函数(IIFE)来实现:
var
arr
= [];
function
f
() {
for
(
var
i
=
0
;
i
<
5
;
i
++){
arr
.
push
((
function
(
i
) {
return
function
() {
console
.
log
(
i
);
}
})(
i
));
}
}
f
();
arr
[
2
]();
通过立即执行匿名函数来保存对应的i值。
现在单地将for循环中的var改为let即可:
var
arr
= [];
function
f
() {
for
(
let
i
=
0
;
i
<
5
;
i
++){
arr
.
push
(
function
() {
console
.
log
(
i
);
})
}
}
f
();
arr
[
2
]();
变量i是let声明的,当前的i只在本轮循环中有效。所以每一次循环的i其实都是一个新的变量。
另外补充一点,在for循环中,其定义循环条件的部分和循环体部分并不是在同一个作用域的,循环条件部分是循环体的父级域,如下例子:
for
(
let
i
=
0
;
i
<
2
;
i
++) {
let
i
=
"CC"
;
console
.
log
(
i
);
}
这段代码能正常执行,输出两次“CC”。
二. const
const除了和let的特性都一样以外,唯一的区别是const定义的常量的值不可更改。这里的常量不可更改是指物理内存地址不可更改,而地址中的内容是可以更改的,这就要求const声明时必须初始化。如下:
const
a
=
1
;
a
=
2
;
console
.
log
(
a
);
以上代码将报错:Uncaught TypeError: Assignment to constant variable. 常量定义以后是不可以再重新赋值的,而常量对象内的变量是重新赋值的,如下:
const
a
= {
name:
'Bob'
};
a
.
name
=
'Chen cong'
;
console
.
log
(
a
.
name
);
此时输出为:Chen cong 。这里改变的是a对象中的内容,而指向对象a的地址并未改变,故可以成功运行。若将a指向另一个对象的话将报错。对于复类型(如数组,对象等)的变量,变量名不指向数据,而是指向数据所在的地址,const命令只保证变量名指向的地址不变,并不保证该地址的数据不变。
备注:若想对象中的内容也不可更改的话,那么可以使Object.freeze()方法来冻结对象。除了将对象本身冻结,对象的属性也应该冻结,如下函数为将一个对象彻底冻结:
var
constantize
=
function
(
obj
) {
Object
.
freeze
(
obj
);
Object
.
keys
(
obj
).
forEach
(
function
(
key
,
value
) {
if
(
typeof
obj
[
key
] ===
'object'
) {
constantize
(
obj
[
key
]);
}
})
}
三. 其他:
let、const、class命令声明的全局变量不属于全局对象的属性。全局变量有两种,在浏览器中指的是window对象,在Node.js中指的是global对象。ES5中,定义全局变量与定义全局对象的属性是等价的。如下:
//ES5
var
a
=
1
;
console
.
log
(
a
);
//1
console
.
log
(
window
.
a
);
//1
//ES6
let
a
=
1
;
console
.
log
(
a
);
//1
console
.
log
(
window
.
a
);
//undefined