ES6 学习笔记 - 块级做用域绑定

本文知识点主要整理自《深刻理解 ES6(Understanding ECMAScript 6)》中文版实体书的内容,部分地方会加上本身的理解,同时书中叙述比较模糊的部分参考了阮一峰老师的《ECMAScript 6 入门》与网络上其余大佬们的博客、问答,篇幅有限没法一一列出,在此表示感谢。javascript

var 声明及变量提高机制

变量提高(Hoisting)

在函数做用域或者全局做用域中使用 var 声明的变量,不管是在哪里进行声明,都会被当成在当前做用域的顶部进行的变量声明,这就是变量提高(Hoisting)。java

如:git

在函数 temp 中声明局部变量,与在函数 temp 后声明全局变量es6

function temp (condition) {
    if (condition) {
        var a = 1; // 函数的局部变量
    } else {
        console.log(a); // undefined
    }
}

var b = 2; // 所有变量
复制代码

当预编译的时候,其实是将上面的代码转化为:github

var b; // 全局变量 b 被提高到全局做用于顶部进行了声明

function temp (conditionP { var a; // 局部变量 a 被提高到函数做用于顶部进行了声明 if (condition) {
        a = 1;
    } else {
        console.log(a); // undefined
    }
}

b = 2;
复制代码

同时因为变量提高的缘由,即使在 tempconditionfalse,在其 else 分支内仍然能够访问到 a数组

块级声明

因为变量提高的存在,对于接触 JavaScript 的人来讲不免会不太习惯,甚至会致使一些 bug。所以 ES6 加入了块级做用域来对变量的声明周期进行控制。网络

ES6 的块级声明做用域指定的区块之间,主要有:app

  • 函数内部
  • 块中(字符 { 与 } 之间)

let 声明

let 的用法与 var 相同,使用 let 进行声明就能将变量的做用域限制在区块中,不会进行变量提高。函数

若是上文的代码中,a 使用 let 进行声明的话,那么在 if 语句中 conditionfalse 时,else 分支将没法访问到 a 的值。工具

if (condition) {
    let a = 1;
} else {
    console.log(a); // Uncaught ReferenceError: a is not defined
}
复制代码

禁止重复声明

同一个做用域内,不能使用 let 再次声明一个已经存在的变量,不论该变量原先是用什么关键字声明的。

var a = 1;
let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared
复制代码

一样,若是是先使用 let 声明,再使用其余关键字声明,一样会报错。

若是都是使用 var 关键字则不会报错

若是是在当前做用域下内嵌另外一个做用域,则能够在内嵌做用域中声明与父级做用域同名的变量,不过在内嵌做用域中的变量在内嵌做用域内会覆盖掉父级做用域的变量值。

const 声明

const 声明的是常量,常量一旦被赋值以后便不可改变。所以,每个常量在被声明的同时 必须进行初始化

constlet 相似,都不会产生变量提高,而且声明的变量都只做用于域块级做用域。同时,若是采用 const 声明已被声明的变量,一样会报错。

let 不一样的是,不论严格模式或者非严格模式下,const 声明过的变量都不能对其 进行改变。

使用 const 声明对象

与其余的语言中的常量很不一样的一点,const 声明的常量虽然不可以改变值,但当使用 const 声明对象时,能够改变对象的属性值。

const person = {
    name: 'Jack',
};

person.name = 'Harry'; // 不会报错

person = { name: 'Peter' }; // 报错
复制代码

在 JavaScript 中,对象是引用数据类型,即上面代码中 person 存储的实际上是对象的引用(引用能够理解为内存地址)。而对 person.name 的值的改变是改变 person 引用的对象,对 person 这个 引用 自己并未做出修改,所以使用 const 声明的 person 并未报错。

而当使用 { name: 'Peter' }person 进行修改时,其实是将 person 的引用 更改 到新对象的内存地址,person 的值被改变了,所以报错。

临时死区

当使用 let 或者 const 声明参数时,在预编译阶段,JavaScript 引擎会将这些参数绑定到其对应的做用域内,而且在执行到声明语句以前若是对这些变量进行操做 均会报错,即使是 typeof

咱们将变量在声明以前所处的封闭区域成为 临时死区暂时性死区 (Temporal Dead Zone,简称 TDZ)。

JavaScript 引擎在预编译时,会将变量提高到做用域顶部(var),或将变量放入 TDZ(letconst)。访问 TDZ 中的变量会报错。当执行变量声明语句后,变量才会从 TDZ 中移除。

注意,TDZ 是绑定做用域的。若是在 TDZ 外访问变量则不会报错。如:

console.log(typeof param); // 输出 'undefined',此处的 param 为全局变量,预编译的时候会有变量提高,所以是 undefined

// JavaScript 引擎在预编译到 if 区块时,会建立一个对应的 TDZ 而且把 param 加入其中
if (condition) {
    param = 9; // 报错,访问了 if 区块内的 TDZ 内的变量 param

    let param = 1; // 此时 param 从 TDZ 中移出,能够访问
}
复制代码

循环中的块做用域绑定

在 ES5 中,在循环内部声明的变量,在循环外部仍旧能够访问。

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

console.log(i); // 10;
复制代码

这也是因为变量提高, i 的声明在预编译时被提到全局做用域顶部,所以在循环结束后外部仍旧能够访问 i

若是采用 let 声明的话,那么 i 在循环结束后就会被销毁,外部没法进行访问。

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

console.log(i); // 报错,没法访问 i
复制代码

循环中的函数

var funcs = [];

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

funcs.forEach(function (func) {
    func();
});
复制代码

以上代码执行后会输出 10 个 10。这是因为变量提高的缘由,i 提高到做用域的顶部,而且在循环外也可以访问,在 for 循环执行完毕后,i 的值为 10,所以在 forEach 遍历数组内的函数而且执行时,均输出 10。

在 ES5 中问了解决该问题,经常使用的方式是使用 当即调用函数表达式(IIFE)。具体写法以下:

var funcs = [];

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

funcs.forEach(function (func) {
    func(); // 0, 1, 2, ..., 10
});

复制代码

在 IIFE 中,将 i 进行值传递(函数的传参是值传递),建立副本而且存储为变量 value ,所以才可以实现正确输出。

循环中的 let 声明

在 ES6 中,能够在 for 循环中直接使用 let 关键字声明,来达到和 IIFE 同样的效果。

for (let i = 0; i < 10; i+= 1) {
    funcs.push(function () {
        console.log(i);
    })
}
复制代码

for 循环中,声明赋值语句 let i = 0 仅在循环以前执行一次(而且这一句执行的时候是在 父级 做用域,与循环内部的做用域是分开的),在执行循环时, JavaScript 内部会记录当前循环的值,在进入下一轮时,会 建立一个新的值,而且用记住的值进行计算而且进入下一轮。所以使用 let 关键词声明的循环在每一次循环时都会获得一个属于该次循环的 副本

for-in 语句使用 let 关键字是也是一样的效果。

let 声明在循环内部的表现是专门定义的,不必定与不产生变量提高的特性相关。

循环中的 const 声明

// 下面会报错
for (const i = 0; i < 10; i += 1) {
    ...
}

// 下面则不会报错
for (const i in obj) {
    ...
}
复制代码

对于 for 循环来讲,每次循环执行完毕,都会去修改 i 的值,而后再建立循环体内块级做用域的副本,所以在 for 循环用 const 声明时会报错。

for-infor-of 在每次迭代时,是每次都会在 新的做用域内 执行 const 声明,不会修改值,所以不会报错。

全局块做用域绑定

使用 var 进行全局变量声明时,会为全局对象建立一个新的属性。以 Web 为例:

var apple = 1;

console.log(window.apple); // 1
复制代码

若是全局对象已经存在属性,则会进行 覆盖(这是一个隐患)。

使用 letconst 在全局做用域声明时,会建立一个新的绑定,而且不会覆盖全局对象已有的属性。

let RepExp = 'Hello';
console.log(RepExp); // Hello
console.log(window.RepExp === RepExp); // false
复制代码

最佳实践

在 ESLint 等代码规范工具中,推荐的写法是 默认使用 const,在确实须要改变值或者引用的时候才使用 let。虽然比较繁琐,可是可以有效下降一些隐性 bug 出现的几率。

详见 ESLint 的 no-varprefer-const 规则

参考资料

相关文章
相关标签/搜索