const、let、var到底什么关系?

前言

在ES5中我们用var定义一个变量时,会出现很多我们不想面对的问题,比如说污染window对象、变量提升导致的错误,污染全局变量、循环中函数迭代问题…,ES6给我们带来了两个全新的声明方式:letconst

var声明下的变量提升

我们先看一个例子:

1
2
3
4
5
6
7
8
9
function log(condition) {
if(condition){
var a = 9;
console.log(a);
}else{
console.log(a);
}
}
log(false); //输出 undefined

按照我们的思想,如果传入false为参数,那么log函数运行的时候是不经过var a =9这里的,因此a就没有定义,那么函数执行到console.log(a)时抛出错误,但是结果却是undefined,这是为什么呢,就是因为javascript又一个变量提升的机制,它将声明的变量直接提升到作用域顶部,也就就是说,javascript引擎会自动给我们奖上一串代码编译为:

1
2
3
4
5
6
7
8
9
10
function log(condition) {
var a;
if(condition){
var a = 9;
console.log(a);
}else{
console.log(a);
}
}
log(false); //输出 undefined

这就很好理解了什么是变量提升机制,它有时候会给我们带来便利,但也会给我们带来预料不到的bug,更多时候我们希望我们能够按照自己的思维去声明变量,按照自己的想法运行在函数中,这个时候,let和const就让我们做到这点。

块级声明(let与const声明)

let声明

针对与上面的变量提升机制,我们想在块级作用域(也叫词法作用域),即{ }( )块和函数中声明的变量在其他的地方是不能被干预的,这个时候,我们就可以方便地使用let与const来声明:

1
2
3
4
5
6
7
8
9
function log(condition) {
if(condition){
let a = 9;
console.log(a);
}else{
console.log(a);
}
}
log(false); //抛出错误

当使用let(也可以用const)声明后,let a = 9;就相当于密闭在if的{ }块级作用域中,别的地方是访问不到的,自然抛出错误。

const声明

const声明一般用于常量的声明,即声明后不可改变其值,如:

1
2
const a = 9;
a = 8; //抛出错误

同时要注意:const声明的时候必须进行初始化,例如以下代码就会抛出错误:

1
2
const a;
a = 9; //抛出错误 SyntaxError: Missing initializer in const declaration

但是!!! 敲黑板:const声明的对象,可以修改对象的属性,注意是修改,不是赋值

以下修改对象的值是完全正确的:

1
2
3
4
5
const student = {
name: 'vince'
};
student.name = 'tony';
console.log(student.name); //输出tony

一下同样也是修改student对象的值:

1
2
3
4
5
const student = {
name: 'huajinbo'
};
student.age = 12;
console.log(student.age); //输出12

但是,给const定义的对象赋值就会抛出错误:

1
2
3
4
5
6
7
8
const student = {
name: 'huajinbo',
age: 19
};
student = {
age: 23
}
console.log(student.age); //跑出错误TypeError: Assignment to constant variable.

let与const禁止重复声明

我们在运行一下代码时:

1
2
3
var a = 9;
var a = 9;
console.log(a); //输出 9

当使用var声明变量的时候,是完全可以重复生命不会出错的,但是在使用let和const时,就会抛出错误

1
2
3
let a = 9;
let a = 9;
console.log(a); //抛出错误:Identifier 'a' has already been declared

另外以下代码也是抛出同样的错误:

1
2
3
const a = 9;
const a = 9;
console.log(a); //抛出错误:Identifier 'a' has already been declared

1
2
3
let a = 9;
const a = 9;
console.log(a); //抛出错误:Identifier 'a' has already been declared

但是在两个不同的作用域中可以重复声明,因为在两个作用域中,变量是彼此不干扰的,如:

1
2
3
4
5
let a = 9;
if(true){
let a = 8;
console.log(a) //输出 8
}

临时死区(TDZ)

这个名字听起来有点吓人,其实它是用来解释let与const去除“变量提升”的原理,为什么用let与const声明就无法进行变量提升呢,原理其实很简单,我们看以下代码:

1
2
console.log(typeof num); //抛出错误
let num = 99;

在同一个作用域下,当javascript识别到num是用let与const声明的时候,当javascript引擎还没执行到let num = 99;时,javascript引擎就会将num变量放倒一个叫“临时死区”的地方,也就是说当javascript引擎还没执行到let num = 99;时要执行console.log(typeof num);时,因为num在临时死区当中,所以就会导致错误

敲黑板!!!

但是如果以上两句代码不再同一作用域下呢,会出现什么结果?
我们看以下代码:

1
console.log(typeof num); //输出undefined

这个就不难理解,因为num没在之前声明,也没在TDZ中,那么就输出undefined

我们再看以下代码:

1
2
3
4
console.log(typeof num);  //输出undefined
if(true){
let num = 9;
}

我们看到console.log(typeof num);let num = 9;是在不同的作用域下,两个语句里面的任何东西都不会相互干扰,因此不会抛出错误

所以,TDZ的前提是“处在同一作用域”下,javascript引擎才会奖还没运行到的let与const申明放到临时死区当中。

循环中的块级作用域绑定

循环中的变量

我们在开发过程中,如果在一个for循环中声明一个变量,那么循环结束后,依然可以在for循环外部访问到这个变量,而且这个值还是最后一次循环所得的值,例如:

1
2
3
4
for(var i = 0; i<4; i++){
...
}
console.log(i); //输出 4

这往往会给我们带来不必要的麻烦,因为我们使用完for循环后很难意识到自己曾经还挖了一个坑,也就是

1
2
3
4
5
``` javascript
for(let i = 0; i<4; i++){
console.log(i);
}
console.log('ddd'+i); //抛出错误

原因很简单,因为let是块级作用域中声明的,在块外自然访问不到。

循环中的函数

如果我们想在循环中将i保存入函数中,我们或许会这样做:

1
2
3
4
5
6
7
8
9
var func = [];
for(var i = 0; i < 8; i++){
func.push(function () {
console.log(i);
})
}
func.forEach(function (func) {
func(); //输出8次“8”
})

这就不是我们期待的结果,为什么会导致这个结果呢?因为循环内部创建的函数都保留了相同对i变量的引用,就像我们上面所说的,在作用域外部,有一个我们“挖的坑”,也就是i = 8,这就不难解释为什么连续输出同样的数值了。

那么如何解决这个问题呢?
在ES5中我们使用到了“立即调用函数”,强制性地生成变量‘副本’,例如:

1
2
3
4
5
6
7
8
9
10
11
var funcs = [];
for(var i = 0; i < 8; i++){
funcs.push((function (value) {
return function () {
console.log(value);
}
}(i)))
}
funcs.forEach(function (func) {
func(); //输出 01234567
})

这个例子我们将循环中的每一个i都创建了一个‘副本’存储到变量value中,这个方法显然不是那么便捷。

let在循环中的新特性

ES6就考虑到这点,给予了let新的特性:

1
2
3
4
5
6
7
8
9
var funcs = [];
for(let i = 0; i < 8; i++){
func.push(function () {
console.log(i);
})
}
funcs.forEach(function (func) {
func(); //输出 01234567
})

这其中又是什么原理呢?我们将这段ES6代码转换为ES5来探一探究竟,转换为ES5后代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use strict";

var funcs = [];

var _loop = function _loop(i) {
funcs.push(function () {
console.log(i);
});
};

for (var i = 0; i < 8; i++) {
_loop(i);
}
funcs.forEach(function (func) {
func(); //输出8次“8”
});

我们在这里可以发现:let声明模仿了上面例子当中的‘立即执行函数’,每一次迭代循环都会创建一个新的变量,并且初始化为当前的值,也就是创建一个副本,让需要i的函数去保存它。这也就是let在循环中的原理。这个特性在for-infor-of同样适用。

const在循环中的新特性

  • for循环下的const

ES6没有明确规定在循环中不能使用const,那我们就来试一试吧,如下代码:

1
2
3
4
5
var func = [];
for(const i = 0; i < 8; i++){
console.log(i);
}
//抛出错误

因为const是声明常量的,当执行i++时,自然会报错

但是,重点来了!!! 敲黑板~!

  • for-infor-of循环下的const:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var funcs = [];

    var object = {
    name: 'vince',
    age: 12,
    sex: 'male'
    }

    for(const key in object){
    funcs.push(function () {
    console.log(key);
    })
    }
    funcs.forEach(function (func) {
    func(); //输出 name age sex
    })

这里我们可以简单地理解为key在作用域中并没有进行改变,只是简单的在每次迭代中创建一个新的绑定。这里的const与let是同样的效果。

window全局作用域下的绑定

在浏览器环境中,我们在全局作用域中用var定义一个变量,浏览器会自动给我们在全局对象(window)对象创建一个属性,即:

1
2
var a = 9;
console.log(window.a); //输出 9

那么问题来了,浏览器这个特性自作多情地给我们创建全局属性,会导致一些不必要的麻烦,比如浏览器下有一个window.RegExp属性,他是浏览器下的正则表达式功能,那么我无意中在全局作用域下用var定义一个RegExp变量呢:

1
2
var RegExp = 'vince';
console.log(window.RegExp); //输出vince

天呐,我们定义的RegExp居然把浏览器原有的正则表达式属性给覆盖了,也就意味着浏览器环境下就不能使用RegExp属性了,这显然不是我们希望的。那么如何解决这个问题呢?

  • 神奇的let与const
    1
    2
    var RegExp = 'vince';
    console.log(window.RegExp); //输出ƒ RegExp() { [native code] }

我们用let或const就完美地解决了这个问题,当我们使用let在浏览器全局环境下声明就阻止浏览器“自作多情”的行为,let真是开发中的好帮手~

总结

  • let与const的声明方式给javascript引入了词法作用域,使得他们不会像var一样将变量提升,这让我们得以更好的把握开发中声明的变量与常量,减少了很多出错的几率,因为变量只会在需要的地方声明,即默认不变的值用const声明,需要改变的值用let声明。

  • 在循环中,let与const在每次迭代中都会创建新的绑定,这样在循环体中创建的函数就能“同步”访问到相应迭代的值。

  • 时代在进步,我们在开发的过程中,应该拥抱ES6给我们带来的便利。