块级做用域绑定(临时死区)(var && let && const 的区别和联系)

导语

过去, javascript 的变量声明机制一直令咱们感到困惑。 大多数类c语言在声明变量的同时也会建立变量(绑定)。 而在之前的javascrpit 中, 什么时候建立变量要看怎么声明变量。 es6 的新语法能够帮助你更好地控制做用域. 本文将解释为何经典的var 容易让人迷惑 。javascript

1 .var 声明&提高机制(Hoisting)

在函数做用域或全局做用域中经过关键字var 声明的变量 , 不管其实是在哪里声明的 , 都会被当成在当前做用域顶部声明的变量 , 这就是咱们常说的提高机制<br>
例如:<br>

```function getValue () {
    if(0) {
        var value = 'blue';
        return value ;
    } else {
        return null
    }
}
`
复制代码

解释 : 若是你不熟悉javascript , 可能会认为只有当condition的值为true 时才会建立变量value 。 事实上, 不管如何变量 value 都会被建立。 在预编译阶段, js 引擎会将上面的getValue函数修改为下面这样:java

`function getValue () {
    var value ; 
    if(0) {
        value = 'blue' ;
        return value ;
    } else {
        return null ;
    }
}
复制代码

变量声明被提高至函数做用域顶部, 而初始化操做依旧留在原处执行, 这就意味着在else子句中也能够访问到该变量 , 并且因为此时变量还没有初始化 , 因此其值为undefined.node


2. 块级声明

块级声明用于声明在指定块的做用域以外没法访问的变量 , 块级做用域存在于 :es6

  • 函数内部浏览器

  • 块中{} 不少类c语言都有块级做用域 , 而es6 引入就上为了让js 更灵活和普适安全

    let 声明

    let 声明的用法与var 相同。 用let 代替var来声明变量, 就能够把变量的做用域限制在当前代码块中 , (稍后咱们将在临时死区一节中讨论另外几处细微的语法差别), 因为let 声明不会被提高, 所以开发者将let 声明语句放在封闭代码块的顶部 , 以便整个代码块均可以访问, 例如 :bash

    function getValue (condition) {
        if(condition) {
            let value = 'blue' ;
            return value ;
        } else {
            //变量value 不存在
            return null
        }
        //变量value 在此处不存在
    }
    复制代码

如今这个getValue 函数的运行结果更像类c语言. 变量value改由关键字let 进行声明后 , 再也不被提高至函数顶部。 执行流离开if 块value马上被销毁。 若是condition 的值false ,就永远不会声明并初始化value.函数

禁止重声明

假设做用域中已经存在某个标志符, 此时再使用let 关键字声明它就会报错 , 好比:ui

var  count = 30 ;
//抛出错误
let count = 10 ;
复制代码

解释:同一做用域中, 不能用let 重复定义已经存在的标志符, 因此此处的let 声明会抛出错误。但若是当前做用域内嵌另外一个做用域, 即可在内嵌的做用域中用let 声明同名变量,例如 :spa

var count = 30 ; 
if(1) {
    //不会抛出错误
    let count = 10 ;
}
复制代码

因为此处的let是在if块内声明了新变量count, 所以不会抛出错误。 内部块中的count会遮蔽全局做用域中的count , 后者只有在if块外才能访问到

const 声明

es6标准还提供了const关键字。使用const 声明的变量是常量,其值一旦被设定后不可更改。所以,每一个经过const声明的常量必须进行初始化,例如

// 有效的常量
const maxItem = 30 ;

//语法错误 , 常量未初始化
const name ;
复制代码

const与let

相同点
const 与let 声明的都是块级标识符,

  1. 因此常量也只在当前代码块内有效,一旦执行到块外会当即被销毁
  2. 在同一做用域用const 声明已经存在的标识符也会致使语法错误,不管该标志符是使用var 仍是let声明的,好比
    var message = 'hello' ;
    let age = 25 ; 
    
    //这俩条都会抛出错误
    const message = 'goodbye' ;
    const age = 30
    
    复制代码

后俩条const 声明语句自己没问题, 但因为前面用var 和let 声明了俩个同名变量,结果代码就没法执行了
尽管类似之处不少, 但有一个很大的不一样,即不管是在严格模式下仍是非严格模式下,都不能够为const定义的常量再赋值, 不然会抛出错误

const maxItems = 5 ;
//抛出错误
maxItem = 6 ;
复制代码

然而js与其余语言不一样的是, javascript中的变量若是是对象,则对象中的值能够修改

用const 声明对象

const声明不容许修改绑定, 但容许修改值,好比

const person = {
    name: 'lihua'
}
//能够修改对象属性的值
person.name = 'hanmeimei'

//抛出错误
person = {
    name:'hanmeimei'
}
复制代码

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

临时死区(Temporal Dead Zone)

与var 不一样,let 和 const 声明的变量不会被提高到做用域顶部, 若是在声明以前访问这些变量, 即时是相对安全的typeof操做符也会触发引用错误,例如:

if(1) {
    console.log(typeof value) ;//引用错误
    let value = 'blue'
}
复制代码

因为console.log(typeof value)语句会抛出错误, 所以用let定义并初始化变量value的语句不会执行。此时的value还位于js所谓的临时死区(TDZ)中。虽然es标准并无明确提到TDZ,但人们经常使用来它来描述let和const的不提高效果。
解释:javaScript引擎在扫描代码发现变量声明中,要么将他们提高至做用域顶部(遇到var 声明),要么将声明放到TDZ中(遇到let 和const声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明的语句后,变量才会从TDZ中移出,而后方可正常访问。
在声明前访问由let定义的变量就是这样。由前面实例可见,即使是相对不容易出错的typeof操做符也没法阻挡引擎抛出的错误。但在let声明的做用域外对该变量使用typeof则不会报错,具体以下:

console.log(typeof value) ;//undefined
    
    if(1) {
        let value = 'blue'
    }
复制代码

typeof 是在声明value的代码块外执行的,此时value并不在TDZ中。这也就意味着不存在value这个绑定,typeof操做最终返回undefined

循环块级做用域绑定

在下文中,一切实例的运行环境均是浏览器 , 而不是node,因此有可能出如今node上不合适的状况
开发者可能最但愿实现for循环的块级做用域了,由于能够把随意声明的计数器变量限制在循环内部。例如:

for (var i = 0 ; i < 10 ; i ++) {
    process(item[i]) ;
}
//这里仍然能够访问变量i
console.log(i) ; // 10
复制代码

在默认拥有块级做用域的其余语言中,这个实例也能够正常运行, 而且变量i只有在for循环中才能访问到。而在javascript中,因为var 声明获得了提高,变量i在循环结束后仍然可访问到。若是换用let声明变量就能获得想要的结果,好比:

for (let i = 0 ; i < 10  ; i ++) {
    process(item[i]) ;
}

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

解释 :在这个实例中, 变量i只存在于for循环中,一旦循环结束,在其余地方均没法访问这个变量

循环中的函数

长久以来, var声明让开发者在循环中建立函数变得异常困难,由于变量到了循环以外仍能访问。请看这段代码:

var funcs = [] ;

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

funcs.forEach ( (func) => {
    func() ; //输出 10 次 数字 10
})
复制代码

你预期的结果多是输出数字 0 - 9 , 但它输出了一连串的10次10 。这是由于循环里的每次迭代同时共享着变量i , 循环内部建立的函数全都保留了对相同变量的引用。循环结束时变量i的值为10, 因此每次调用console.log(i) 时就会输出数字10

解决方法1: 自执行函数

为了解决这个问题,开发者们在循环中使用当即执行函数表达式,以强制生成计数器变量的副本,就像这样:

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 而后1 ,2 , ....  到 9 
}) ; 
复制代码

解释: 在循环内部自执行函数为接受的每个变量i都建立了一个副本并存储为变量value。这个变量的值就是相应迭代建立的函数所使用的值。所以调用每一个函数都会像0到9循环同样获得指望的值。es6中的let和const提供的块级绑定让咱们无须这么折腾

解决方法2: let && const

let声明模仿上述实例中自执行函数所作的一切来简化循环过程。每次迭代循环都会建立一个新变量,并以以前迭代中同名变量的值将其初始化。这意味着你完全删除自执行函数以后仍然能够获得预期的结果,就像这样:

var funcs = [] ;

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

funcs.forEach( func =>{
    func() ; //输出 0 1  2  ... 9
})
复制代码

这段循环与以前那段结合了var和自执行函数的循环的运行结果相同,但相比之下更为简洁。每次循环的时候let声明都会建立一个新变量i,并将其初始化为i的当前值。因此循环内部建立的每一个函数都能获得属于他们本身的i的副本。对于for-in 和 for-of 来讲也是同样的。
let声明在循环内部的行为是标准中专门定义的,它不必定与let的不提高特性有关,理解这一点相当重要。事实上,早期的let实现不包含这一行为,它是后来加入的

全局块做用域绑定

let和const与var的另一个区别是它们在全局做用域中的行为。当var被用于全局做用域时,它会建立一个新的全局变量做为全局对象(浏览器环境中的window对象)的属性。这意味着用var 极可能会无心中覆盖一个已经存在的全局变量,就像这样:

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

var ncz = "Hi!" ;
console.log(window.ncz) ; // "Hi!"
复制代码

即便全局对象RegExp定义在window上,也不能幸免被var声明覆盖。实例中声明的全局对象RegExp会覆盖以前原来那个。一样,ncz被定义为一个全局变量,并当即成为window的属性。javaScript过去一直这样。
若是你在全局做用域中使用let或const,会在全局做用域下建立一个新的绑定,但该绑定不会添加为全局对象的属性。换句话说,用let或const不能覆盖全局变量,而只能遮蔽它。实例以下:

//在浏览器中
let RegExp = "hello!" ;
console.log(RegExp) ;    //  "hello"
console.log(RegExp === window.RegExp)  //   false

const  ncz = "Hi!" ;
console.log(ncz) ;   //  "Hi!" 
console.log("ncz" in window)  //  false
复制代码

这里let声明的RegExp建立了一个绑定并遮蔽了全局的RegExp变量。结果是window.RegExp和RegExp不相同,但不会破坏全局做用域。一样,const声明的ncz建立了一个绑定但没有建立为全局对象的属性。若是不想为全局对象建立属性,则使用let和const要安全得多。
Note:若是但愿在全局对象下定义变量,仍然可使用var。这种状况常见于在浏览器中跨frame或跨window访问代码。

小结

块级做用域绑定的let和const为javaScript引入了词法做用域,他们声明的变量不会提高,并且只能够在声明这些变量的代码块中使用。如此一来,javascript声明变量的语法与其余语言就更像了。同时也下降了产生错误的可能。与此同时,在声明前访问块级变量会致使错误,由于变量还在临时死区(TDZ)中。
let和const的行为很时候与var一致。然而,他们在循环中的行为很不同。在for循环中,let和const都会每次迭代建立新绑定,从而使循环体内建立的函数能够访问到相应迭代的值,而非最后一次迭代后的值(像var声明那样)。

关于做者

本文部分借鉴了其余大佬的做品(哈哈),只是为了分享和复习,侵权立删

相关文章
相关标签/搜索