qingely / study

学习笔记
0 stars 0 forks source link

深入理解 ES6 - 块级绑定 #3

Open qingely opened 6 years ago

qingely commented 6 years ago

ES6 之前,我们用 var 声明变量,用 function 来声明函数。这两种声明方式都有 变量提升 的效果。

var 声明变量具有以下特点:

  • 有变量提升效应;
  • 无块级作用域,因而一般为全局变量(函数内部声明的变量除外)
  • 重复声明同一变量,不会报错;
  • 在全局作用域上声明的变量会成为全局对象的属性。

块级声明

ES6 开始,引入了块级作用域,让变量的生命周期更加可控。块级作用域(又被称为词法作用域)在如下情况被创建:

块级声明,即声明的变量只在在指定的块级作用域内有效,在指定的块级作用域外是无法被访问的。ES6 提供了两种声明方式,来实现块级声明:let 声明 - 声明变量、const 声明 - 声明常量 & 一般对象。

let 声明const 声明

共同点:

  • 无变量提升效应,因而在声明语句之前的代码是无法访问的该变量的。
  • 只在当前块级作用域内有效,外部无法访问;
  • 同一作用域级别下,重复声明同一变量会报错。
  • 在全局作用域上使用 let 或 const ,虽然在全局作用域上会创建新的绑定,但不会有任何属性被添加到全局对象上。这也就意味着你不能使用 let 或 const 来覆盖一个全局变量,你只能将其屏蔽。
// 在浏览器中,全局作用域下 var 的情况
var RegExp = "Hello!";
console.log(window.RegExp);     // "Hello!"
var ncz = "Hi!";
console.log(window.ncz);           // "Hi!"
// 在浏览器中,全局作用域下 let 与 const 的情况
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 声明的是常量,必须初始化,且之后不能重新对该常量赋值(若该常量为对象,对象本身不行,但对象成员的值是可以修改的)。

变量提升的实质:当 JS 引擎检视接下来的代码块并发现变量声明时,它会在面对 var 的情况下将声明提升到函数或全局作用域的顶部,而面对 let 或 const 时会将声明放在暂时性死区内。任何在暂时性死区内访问变量的企图都会导致“运行时”错误(runtime error)。只有执行到变量的声明语句时,该变量才会从暂时性死区内被移除并可以安全使用。

循环语句中 var 与 let, const 的比较

for 循环

var 声明

for (var i = 0; i < 10; i++) {
process(items[i]);
}
// i 在此处仍然可被访问
console.log(i); // 10

let 声明

for (let i = 0; i < 10; i++) {
    process(items[i]);
}
// i 在此处不可访问,抛出错误
console.log(i);

长期以来, var 的特点使得循环变量在循环作用域之外仍然可被访问,于是在循环内创建函数就变得很有问题。考虑如下代码:

var funcs = [];
for (var i = 0; i < 10; i++) {
    funcs.push(function() { console.log(i); });
}
funcs.forEach(function(func) {
    func(); 
});  // 输出数值 "10" 十次

这是因为变量 i 在循环的每次迭代中都被共享了,意味着循环内创建的那些函数都拥有对于同一变量的引用。在循环结束后,变量 i 的值会是 10 ,因此当 console.log(i) 被调用时,每次都打印出 10 。

为了修正这个问题,开发者在循环内使用立即调用函数表达式(IIFEs),以便在每次迭代中强制创建变量的一个新副本,示例如下:

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 依次输出

这种写法在循环内使用了 IIFE 。变量 i 被传递给 IIFE ,从而创建了 value 变量作为自身 副本并将值存储于其中。 value 变量的值被迭代中的函数所使用,因此在循环从 0 到 9 的过程中调用每个函数都返回了预期的值。幸运的是,使用 let 与 const 的块级绑定可以在ES6 中为你简化这个循环。

let 声明通过有效模仿上例中 IIFE 的作用而简化了循环。即 let 在每次迭代中,都会创建一个新的同名变量并对其进行初始化。这意味着你可以完全省略 IIFE 而获得预期的结果,就像这样:

var funcs = [];
for (let i = 0; i < 10; i++) {
    funcs.push(function() {
        console.log(i);
});
}
funcs.forEach(function(func) {
    func(); 
})  // 从 0 到 9 依次输出

与使用 var 声明以及 IIFE 相比,这里代码能达到相同效果,但无疑更加简洁。在循环中, let 声明每次都创建了一个新的 i 变量,因此在循环内部创建的函数获得了各自的 i 副本,而每个 i 副本的值都在每次循环迭代声明变量的时候被确定了。这种方式在 for-infor-of 循环中同样适用,如下所示:

var funcs = [],
object = {
    a: true,
    b: true,
    c: true
};
for (let key in object) {
    funcs.push(function() {
        console.log(key);
    });
}
funcs.forEach(function(func) {
    func(); 
});  // 依次输出 "a"、 "b"、 "c"

本例中的 for-in 循环体现出了与 for 循环相同的行为。每次循环,一个新的 key 变量绑定就被创建,因此每个函数都能够拥有它自身的 key 变量副本,结果每个函数都输出了一个不同的值。而如果使用 var 来声明 key ,则所有函数都只会输出 "c" 。

for-infor-of 循环中,const 与 let 的效果一样。唯一的区别是 key 的值在 循环内不能被更改。 const 能够在 for-in 与 for-of 循环内工作,是因为循环为每次迭 代创建了一个新的变量绑定,而不是试图去修改已绑定的变量的值(就像使用了 for 而不是for-in 的上个例子那样)。

需要重点了解的是:let 声明在循环内部的行为是在规范中特别定义的,而与不提升变量声明的特征没有必然联系

块级绑定的最佳实践

在默认情况下使用 const ,且只在知道变量值需要被更改的情况下才使用 let 。其理论依据是大部分变量在初始化之后都不应当被修改,因为预期外的改动是 bug 的源头之一。这种理念有着足够强大的吸引力,在你采用 ES6 之后是值得在代码中照此进行探索实践的。