小记2 - 循环中的块级绑定

2019-12-12  本文已影响0人  ilily

背景 - 一个现象

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

上面现象:JS中,变量 i 泄露为全局变量,循环结束后 i 仍可被访问,因为 var 声明导致了变量提升。
我们最需要使用变量的块级作用域的场景,或许就是在for循环内 -- 想让一次性的循环计数器仅能在循环内部使用。

换为使用 let ,则会看到预期行为:
for (let i=0; i<10; i++)    {               
    process(items[i]); 
}  
// i 在此处不可访问,抛出错误
 console.log(i);  //  Uncaught ReferenceError: process is not defined

本例中的变量 i 仅在 for 循环内部可用,一旦循环结束,该变量在任意位置都不可访问。

一、循环内的函数问题

  1. ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
// 情况一
if (true) {
  function f() {}
}
// 情况二
try {
  function f() {}
} catch(e) {
  // ...
}

上面两种函数声明,根据 ES5 的规定都是非法的。
但,浏览器没有遵守这个规定,为兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。

  1. ES6 引入了块级作用域,明确允许在块级作用域中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。(后续有补充)
  2. var 的特点使得循环变量在循环作用域之外仍然可被访问,于是在循环内创建函 数就变得很有问题。考虑如下代码:
var funcs = [ ];
for (var i=0; i<10; i++)    {               
     funcs.push(function()  {   
          console.log(i);   
     }); 
}
funcs.forEach(function(func)    {               
     func();        //  输出数值    "10"    十次 
});

我们预期/期望会输出 0 到 9 的数值,但却在同一行将数值 10 输出了十次。

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

  1. 为修正 3. 中这个问题,我们想到了在循环内使用立即调用函数表达式(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 变量作为i的 副本并将值存储于其中。
value 变量的值被迭代中的函数所使用,因此在循环从 0 到 9 的过 程中调用每个函数都返回了预期/期望的值。

  1. 在 ES6 中,使用 let 与 const 的块级绑定可以简化这个循环。

二、循环内的 let 声明

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-in 和 for-of 循环中同样适用,如下所示:

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

for-in 循环体现出了与 for 循环相同的行为。
每次循环,一个新的 key 变量绑 定就被创建,因此每个函数都能够拥有它自身的 key 变量副本,结果每个函数都输出了一个 不同的值。
而如果使用 var 来声明 key ,则所有函数都只会输出 "c" 。
需要重点了解的是: let 声明在循环内部的行为是在规范中特别定义的,而与不提升变 量声明的特征没有必然联系。事实上,在早期 let 的实现中并没有这种行为,它是后来 才添加的。

三、循环内的const声明

ES6 规范没有明确禁止在循环中使用 const 声明,然而它会根据循环方式的不同而有不同行 为。

  1. 在常规的 for 循环中,你可以在初始化时使用 const ,但循环会在你试图改变该变量 的值时抛出错误。
var funcs = [];
//  在一次迭代后抛出错误 
for (const i= 0; i<10; i++) {   
     funcs.push(function()  {                                    
         console.log(i);                
      }); 
}

在此代码中, i 被声明为一个常量。循环的第一次迭代成功执行,此时 i 的值为 0 。在 i++ 执行时,一个错误会被抛出,因为该语句试图更改常量的值。因此,在循环中你只能使 用 const 来声明一个不会被更改的变量。

  1. const 变量在 for-in 或 for-of 循环中使用时,与 let 变量效果相同。因 此下面代码不会导致出错:
var funcs = [], 
    obj =   {                               
        a:  true,                               
        b:  true,                               
        c:  true                
     };
//  不会导致错误
for (const  key in  obj)    {               
    funcs.push(function() {                                        
        console.log(key);               
    }); 
}
funcs.forEach(function(func)    {               
    func();  // 依次输出    "a"、    "b"、    "c"
 });

这段代码与“循环内的 let 声明”中几乎完全一样,唯一的区别是 key 的值在 循环内不能被更改。
const 能够在 for-in 与 for-of 循环内工作,是因为循环为每次迭 代创建了一个新的变量绑定,而不是试图去修改已绑定的变量的值。

PS:let 、const 与 var 在全局作用域上的表现区别。

当在全局作用域上使 用 var 时,var会创建一个新的全局变量,并成为全局对象(在浏览器中是 window )的一 个属性。这意味着使用 var 可能会无意覆盖一个已有的全局属性,

//  在浏览器中 
var RegExp  =   "World!";
console.log(window.RegExp   );      //"World!"
var hisName=    "ZhangSan!"; 
console.log(window.hisName);        //  "ZhangSan!"

虽然全局的 RegExp 是定义在 window 上的,但它仍不能防止被 var 重写。上例声明 了一个新的全局变量 RegExp 而覆盖了window上原有的RegExp 对象。
类似的, hisName定义为全局变量后就立即 成为了 window 的一个属性。这就是 JS 通常的工作方式。

区别:在全局作用域上使用 let 或 const声明变量 ,虽在全局作用域上会创建新的绑定,但不 会有任何属性被添加到全局对象上。这也就意味着你不能使用 let 或 const 来覆盖一个全 局变量,你只能将其屏蔽。这里有个范例:

//  在浏览器中 

let RegExp  =   "Hello!"; 
console.log(RegExp);    //  "Hello!"        
console.log(window.RegExp   === RegExp);        //  false
                                                             
const   herName=    "Lisi!"; 
console.log(herName);       //  "Lisi!"     
console.log("herName"   in  window);        //  false

let 声明创建了 RegExp 的一个绑定,并屏蔽了全局的 RegExp 。这表示 window.RegExp 与 RegExp 是不同的,因此全局作用域没有被污染。
同样, const 声明创建 了 herName的一个绑定,但并未在全局对象上创建属性。当不想在全局对象上创建属性时,这 种特性会让 let 与 const 在全局作用域中更安全。

若想让代码能从全局对象中被访问,你仍然需要使用 var 。在浏览器中跨越帧或窗口去 访问代码时,这种做法非常普遍。

总结:

let 与 const 的很多情况下都相似于 var ,但在循环中和全局作用域中和var表现都有区别。

  1. 在 for-in 与 for-of 循环中, let 与 const 都能在每一次迭代时创建一个新的绑定,这意味着在循 环体内创建的函数可以使用当前迭代所绑定的循环变量值(而不是像使用 var 那样,统一使 用循环结束时的变量值)。这一点在 for 循环中使用 let 声明时也成立,不过在 for 循 环中使用 const 声明则会导致错误。
  2. 在全局作用域上使用 let 或 const声明变量 ,虽在全局作用域上会创建新的绑定,但不 会有任何属性被添加到全局对象window上。
    而var在全局作用域上声明的变量会挂载到window上,成为window的一个属性
上一篇下一篇

猜你喜欢

热点阅读