深入理解ES6——块级作用域绑定
文章目录
前言
在ES5中我们用var定义一个变量时,会出现很多我们不想面对的问题,比如说污染window对象、变量提升导致的错误,污染全局变量、循环中函数迭代问题…,ES6给我们带来了两个全新的声明方式:let与const
var声明下的变量提升
我们先看一个例子:1
2
3
4
5
6
7
8
9function 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
10function 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
9function 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
2const a = 9;
a = 8; //抛出错误
同时要注意:const声明的时候必须进行初始化,例如以下代码就会抛出错误:1
2const a;
a = 9; //抛出错误 SyntaxError: Missing initializer in const declaration
但是!!! 敲黑板:const声明的对象,可以修改对象的属性,注意是修改,不是赋值
以下修改对象的值是完全正确的:1
2
3
4
5const student = {
name: 'vince'
};
student.name = 'tony';
console.log(student.name); //输出tony
一下同样也是修改student对象的值:1
2
3
4
5const student = {
name: 'huajinbo'
};
student.age = 12;
console.log(student.age); //输出12
但是,给const定义的对象赋值就会抛出错误:1
2
3
4
5
6
7
8const student = {
name: 'huajinbo',
age: 19
};
student = {
age: 23
}
console.log(student.age); //跑出错误TypeError: Assignment to constant variable.
let与const禁止重复声明
我们在运行一下代码时:1
2
3var a = 9;
var a = 9;
console.log(a); //输出 9
当使用var声明变量的时候,是完全可以重复生命不会出错的,但是在使用let和const时,就会抛出错误1
2
3let a = 9;
let a = 9;
console.log(a); //抛出错误:Identifier 'a' has already been declared
另外以下代码也是抛出同样的错误:1
2
3const a = 9;
const a = 9;
console.log(a); //抛出错误:Identifier 'a' has already been declared
1 | let a = 9; |
但是在两个不同的作用域中可以重复声明,因为在两个作用域中,变量是彼此不干扰的,如:1
2
3
4
5let a = 9;
if(true){
let a = 8;
console.log(a) //输出 8
}
临时死区(TDZ)
这个名字听起来有点吓人,其实它是用来解释let与const去除“变量提升”的原理,为什么用let与const声明就无法进行变量提升呢,原理其实很简单,我们看以下代码:1
2console.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
4console.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
4for(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
9var 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
11var 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
9var 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;
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-in和for-of同样适用。
const在循环中的新特性
- for循环下的const
ES6没有明确规定在循环中不能使用const,那我们就来试一试吧,如下代码:1
2
3
4
5var func = [];
for(const i = 0; i < 8; i++){
console.log(i);
}
//抛出错误
因为const是声明常量的,当执行i++时,自然会报错
但是,重点来了!!! 敲黑板~!
for-in域for-of循环下的const:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var 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
2var a = 9;
console.log(window.a); //输出 9
那么问题来了,浏览器这个特性自作多情地给我们创建全局属性,会导致一些不必要的麻烦,比如浏览器下有一个window.RegExp属性,他是浏览器下的正则表达式功能,那么我无意中在全局作用域下用var定义一个RegExp变量呢:1
2var RegExp = 'vince';
console.log(window.RegExp); //输出vince
天呐,我们定义的RegExp居然把浏览器原有的正则表达式属性给覆盖了,也就意味着浏览器环境下就不能使用RegExp属性了,这显然不是我们希望的。那么如何解决这个问题呢?
- 神奇的let与const
1
2var RegExp = 'vince';
console.log(window.RegExp); //输出ƒ RegExp() { [native code] }
我们用let或const就完美地解决了这个问题,当我们使用let在浏览器全局环境下声明就阻止浏览器“自作多情”的行为,let真是开发中的好帮手~
总结
let与const的声明方式给javascript引入了词法作用域,使得他们不会像var一样将变量提升,这让我们得以更好的把握开发中声明的变量与常量,减少了很多出错的几率,因为变量只会在需要的地方声明,即默认不变的值用const声明,需要改变的值用let声明。
在循环中,let与const在每次迭代中都会创建新的绑定,这样在循环体中创建的函数就能“同步”访问到相应迭代的值。
时代在进步,我们在开发的过程中,应该拥抱ES6给我们带来的便利。