【ES6系列】块级作用域绑定

387 阅读4分钟

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

  1. 在函数作用域或全局作用域中通过关键字var声明的变量,无论实际上是在哪里声明的,都会被当成在当前作用域顶部声明的变量,这就是hoisting机制
  2. 变量的声明会被提升至函数顶部,但是初始化操作依旧留在原处执行

块级声明

  1. 块级声明用于声明在指定块的作用域之外无法访问的变量,块级作用域(词法作用域)存在于:

    • 函数内部
    • 块中
  2. let声明

    let声明的语法和var相同,用let代替var,就可以把变量的作用域限制在当前代码块中,且let声明不会被提升

  3. 禁止重复声明

    • 如果作用域中已经存在某个标识符,此时再使用let关键字声明就会抛出错误
    • 同一作用域中不能用let重复定义已经存在的标识符,所以let会抛出错误
    • 如果当前作用域内嵌另一个作用域,便可以在内嵌的作用域中用let声明同名变量,且不会抛出错误,此时内嵌作用域的变量会覆盖当前作用域的变量
  4. const声明

    • const关键字声明的是常量,其值一旦被设定后就不可更改,因此每个通过const声明的变量必须进行初始化
    • const和let声明的都是块级标识符,所以常量也只是在当前代码块内有效,一旦执行到外块会被立即销毁。
    • 常量声明同样也不会被提升至作用域顶部
    • 与let相似,const也不支持重复声明
    • 如果const声明的变量的值是对象, 则对象中的值可以修改
    • const声明不允许修改绑定,但是允许修改值
  5. 临时死区(Temporal Dead Zone~TDZ)

    与var不同,let和const声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,即使是相对安全的typeof操作符也会触发引用错误

    if(condition){
      console.log(value)//引用错误
      let value = 'blue'
    }
    

    由于console语句抛出错误,因此let定义并初始化变量value的语句不会执行,此时的value还位于临时死区中

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

    console.log(typeof value) // undefined
    if(condition){
      let value = 'blue'
    }
    

    在let所在块级作用域内,let声明之前访问,会导致运行时错误

    在let所在块级作用域外,不会导致运行时错误,undefined

循环中的块作用域绑定

  1. 循环中的函数

    困境:var声明使得在循环中创建函数变的异常困难

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

    此时会输出十次数字10,因为这时访问的都是循环中var声明的同一个变量i

    解决:在循环中使用立即调用函数表达式(IIFE),用以强制生成计数变量的副本

    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

  2. 循环中的let声明

    通过let声明解决上述问题

    var funcs = []
    for(let i = 0;i<10;i++){
        funcs.push(function(){
            console.log(i)
        })
    }
    funcs.forEach(function(func){
        func()
    })
    

    此时会输出0~9

    let声明模仿上述IIFE所做的一切来简化循环过程,每次迭代循环都会创建一个新的变量,并以之前迭代中同名变量的值将其初始化,所以循环内部创建的每个函数都能得到属于他们自己的i的副本

  3. 循环中的const声明

    循环中的const的行为与let行为非常类似,区别就是循环中使用const的时候,不可以对const声明的值进行修改

全局块作用域绑定

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

    • 当var被用于全局作用域时,他会创建一个新的全局变量作为全局对象(浏览器环境中的window对象),如下

      var RegExp = 'hello'
      console.log(window.RegExp) // hello
      

      var声明的全局变量会覆盖已经存在的全局属性

    • 全局作用域中使用let或者const,会在全局作用域下创建一个新的绑定,但是该绑定不会添加为全局对象的属性,换句话说,用let或const不会覆盖全局变量,只能 遮蔽

      let RegExp = 'hello'
      console.log(window.RegExp) // ƒ RegExp() { [native code] }
      console.log(RegExp) // 'hello'
      

      这里let创建了一个绑定并遮蔽了全局的RegExp变量,但是不会破坏全局作用域

    • 如果不想为全局对象创建属性,使用let和const要安全得多

块级绑定最佳实践的进化

默认使用const,变量确实需要改变时才使用let,可以在某种程度上实现代码的不可变,从而防止某些错误的产生