《深入理解ES6》第1章-块级作用域绑定

var声明及变量提升(Hoisting)机制

提升机制:在函数作用域或全局作用域中通过关键字var声明的变量以及关键字function声明的函数,无论实际上是在哪里声明,都会被当成在当前作用域顶部声明的变量。
注意:提升机只是提升声明操作,初始化操作依旧留在原处执行。

块级声明

块级作用域,也称为词法作用域,用于声明在指定块的作用域之外无法访问的变量,其存在于:

  • 函数内部
  • 块中(字符{和}之间的区域)

let声明

let声明,可以把变量的作用域限制在当前代码块,且通常放在封闭代码块的顶部。

1
2
3
4
5
6
7
8
9
10
11
12
function getValue(condition){
if(condition){
let value="blue";
//其他代码
return value;
}else{
//变量value在此处不存在
return null;
}

//变量value在此处不存在
}

执行流离开if块,value立刻被销毁。

禁止重声明

假设作用域中已经存在某个标识符(由关键字varfunction创建),此时再使用let关键字声明它会抛出错误。

1
2
3
4
var count=30;

//抛出语法错误
let count=40;

但是可以在内嵌的作用域中用let声明同名变量

1
2
3
4
5
6
7
8
function foo(){};

if(condition){

//不会抛出错误
let foo=40;
//更多代码
}

内部块中的count会遮蔽全局作用域中的count,后者只有在if块外才能访问到。

const声明

const声明的是常量,其值一旦被设定后不可更改,且每个const声明的常量必须进行初始化。

const与let

  1. const与let相同点:
  • 都是块级作用域,不会提升,只在当前代码块有效
  • 声明已经存在的标识符(varletfunction )会导致语法错误
  1. const与let不同点:
  • 无论在严格模式还是非严格模式,都不可以为const定义的常量再赋值(但对象中的值可以修改)。

用const声明对象

记住const声明不允许修改绑定,但允许修改值,意味着用const声明对象后,可以修改该对象的属性值。

1
2
3
4
5
6
7
8
9
10
11
const person={
name:"Nicholas"
};

//可以修改对象属性的值
person.name="Greg";

//抛出语法错误
person={
name:"Greg"
};

如果直接给person赋值,即要改变person的绑定,就会抛出错误。
切记const声明不允许修改绑定,但允许修改绑定的值。

临时死区(Temporal Dead Zone)

先看例子:

1
2
3
4
if(condition){
console.log(typeof value); //引用错误! 下面语句不会执行
let value="blue";
}

什么是临时死区?
JS引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(遇到var声明或function),要么将声明放到TDZ中(遇到letconst声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后方可正常访问。

但在let声明的作用域外对该变量使用typeof则不会报错

1
2
3
4
5
console.log(typeof value);   //"undefined"

if(condition){
let value="blue";
}

此时value并不在TDZ中,也意味着不存在value与这个块级作用域的绑定。

循环中的块级作用域绑定

开发者希望实现for循环的块级作用域,这样可以把随意声明的计数器变量限制在循环内部。

1
2
3
4
5
6
for(let i=0;i<10;i++){
process(item[i]);
}

//i在这里不可访问,抛出一个错误
console.log(i);

循环中的函数

看这段代码

1
2
3
4
5
6
7
8
9
10
11
var funcs=[];

for(var i=0;i<10;i++){
funcs.push(function(){
console.log(i);
});
}

funcs.forEach(function(func){
func(); //输出10次数字10
});

循环里每次迭代同时共享着变量i,循环内部创建的函数全部都保留了对相同变量的引用。
可以使用IIFE来解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var funcs=[];

for(var i=0;i<10;i++){
funcs.push((function(value){
return function(){
console.log(value);
}
}(i)));
}

funcs.forEach(function(func){
func(); //输出0-9
});

//或者等同于
var funcs=[];

for(var i=0;i<10;i++){
(function(value){
funcs.push(function(){
console.log(value);
});
})(i);
}

funcs.forEach(function(func){
func(); //输出0-9
});

在循环内部,IIFE表达式为接受的每一个变量i都创建了一个副本并储存为变量value。

循环中的let声明

let声明,每次迭代循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化:

1
2
3
4
5
6
7
8
9
10
11
var funcs=[];

for(let i=0;i<10;i++){
funcs.push(function(){
console.log(i);
});
}

funcs.forEach(function(func){
func(); //输出0-9
});

每次循环的时候let声明都会创建一个新变量i,并将其初始化为i的当前值,所有内部循环创建的每个函数都得到属于自己的i的副本。

对于for-in循环和for-of循环也一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var funcs=[],
object={
a:true,
b:true,
c:true
};

for(let key in object){
funcs.push(function(){
consle.log(key);
});
}

funcs.forEach(function(func){
func(); //输出a、b、c
});

每次循环创建一个新的key绑定,因此每个函数都有一个变量key的副本,于是每个函数输出不同的值。
如果使用var声明key,则这些函数会输出“c”。

注意:let声明在循环内部的行为是标准中专门定义的,它不一定与let的不提升特性相关。

循环中的const声明

针对不同类型的循环const会表现出不同的行为:

  • 普通的for循环,可以在初始化变量时使用const,但是更改这个变量的值会抛出错误。
    1
    2
    3
    4
    5
    var funcs=[];

    for(const i=0;i<10;i++){
    console.log(i); //0,完成一次迭代后抛出错误
    }

在循环的第一个迭代中,i是0,迭代执行成功。然后执行i++,因为该语句试图修改常量,因此抛出错误。

for-infor-of循环中使用const时的行为与使用let一致。
不会报错的原因,每次迭代不会修改已有绑定,而是会创建一个新绑定。

全局块作用域绑定

let和const,与var的另一个区别是他们在全局作用域中的行为。

  • 当var被用于全局作用域时,他会创建一个新的全局变量作为全局对象的不可配置属性(不能删除)。
  • 当let const用于全局作用域时,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。即,不能覆盖全局变量,而只能遮蔽。
    1
    2
    3
    4
    5
    6
    7
    let RegExp="hello";
    console.log(RegExp); //"hello"
    console.log(window.RegExp===RegExp); //false

    const ncz="hi";
    console.log(ncz); //"hi"
    console.log("ncz" in window); //false

块级绑定最佳实践的进化

一开始人们认为:
默认使用let,对于需要保护的变量则要使用const。
后来人们认为:
默认使用const,只有确实需要改变变量的值时使用let。

小结

letconst块级绑定将词法作用域引入了JS。这两种声明方式都不会进行提升,并且
只会在声明它们的代码块内部存在。由于变量能够在必要位置被准确声明,其表现更加接近
其他语言,并且能减少无心错误的产生。作为一个副作用,你不能在变量声明位置之前访问
它们,即便使用的是typeof这样的安全运算符。由于块级绑定存在暂时性死区(TDZ ),
试图在声明位置之前访问它就会导致错误。

letconst的表现在很多情况下都相似于var,然而在循环中就不是这样。在for-infor-of循环中,letconst都能在每一次迭代时创建一个新的绑定,这意味着在循
环体内创建的函数可以使用当前迭代所绑定的循环变量值(而不是像使用var那样,统一使
用循环结束时的变量值)。这一点在for循环中使用let声明时也成立,不过在for
环中使用const声明则会导致错误。

块级绑定当前的最佳实践就是:在默认情况下使用const,而只在你知道变量值需要被更改
的情况下才使用let。这在代码中能确保基本层次的不可变性,有助于防止某些类型的错
误。

liborn wechat
欢迎您扫一扫上面的微信二维码,订阅我的公众号!