3

var声明和变量提升

在函数作用域或全局作用域中通过var声明的变量,无论实际声明位置在哪里,都会被当成在当前作用域顶部声明的变量,这就是提升机制。

function getValue(condition){
    if(condition){
        var value='blue';
        ....
        return value;
    }else{
        //这里可以访问到value,但是它的值是undefined
        return null;
    }
    //这里也可以访问到value,值也是undefined,因为在预编译阶段,js引擎会将var声明的变量value提升到当前作用域(函数作用域顶部),if代码块里只是它的初始化赋值操作
}

块级声明

块级声明用于声明在指定块的作用域之外无法访问的变量。它存在于:

  • 函数内部
  • 代码块中(字符{和}之间)

let声明

用let代替var来声明变量,就可以把变量的作用域限制在当前代码块中,注意,let声明不会被提升。

function getValue(condition){
    if(condition){
        let value='blue';
        ...
        return value;
    }else{
        //value在这里不存在
        return null;
    }
    //value在这里也不存在
}

tips:当前作用域中已经存在某个标识符,此时再使用let关键字声明它就会抛出错误,但如果当前作用域内嵌另一个作用域,便可以在内嵌的作用域中用let声明同名变量(已声明的变量用var再次声明并不会报错)

const声明

使用const声明的是常亮,其值一旦被声明便不可更改。因此,每个通过const声明的常量必须进行初始化,只声明但未对其初始化会抛出语法错误。

const和let

const和let都是块级标识符,一旦执行到块外会立刻被销毁。
总结几个共同点:

  1. 都有块级作用域
  2. 都不会进行变量提升
  3. 都不可重复声明

tips:const声明的是常亮,初始化后值不修改,但如果const声明的值为数组或对象,则该对象的属性或改数组元素是可以修改的(个人理解:const声明的变量是存的该对象或数组的指针)

临时死区(Temporal Dead Zone)

let和const是不会有声明提升的,如果在声明前访问这些变量,即使是相对安全的typeof操作符也会触发引用错误

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

clipboard.png

解释:js引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(var),要么将声明放到TDZ(临时死区)中(let和const)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移除,然后方可正常访问。
tips:typeof在声明变量的代码块外执行的时候,变量还不在TDZ中,所以typeof不会报错,只是结果是undefined

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

循环中的块作用域绑定

先来看个例子:

for(var i=0;i<10;i++){
    process(items[i]);
}
console.log(i); //打印出10

因为var声明使得i得到了提升,所以变量i在循环结束后仍可访问,所以会打印出10。

for(let i=0;i<10;i++){
    process(items[i]);
}
console.log(i);

let声明的结果是正确的。

一个常见的面试题

请编写一段代码,每隔一秒输出一个数字,到0-9结束,一个非常大众化的错误答案:

for(var i=0;i<10;i++){
    setTimeout(function(){
        console.log(i); //10次10
    },i*1000);
}

因为使用var声明的变量会被提升,for循环执行完毕去执行event loop队列中的setTimeout时,console.log访问到的i是循环结束后的10,所以会打印10次10,并不符合我们的预期。
一种解决办法:

for(var i=0;i<10;i++){
    (function(value){
        setTimeout(function(){
            console.log(value);
        },value*1000);
    })(i)
}

在循环中使用立即调用表达式(IIFE),强制生成计数器变量的副本,在循环内部,IIFE表达式为接收的每一个变量i都创建了一个副本,并存储为变量value。
使用let变量可以轻松解决这个问题:

for(let i=0;i<10;i++){
    setTimeout(function(){
        console.log(i); //10次10
    },i*1000);
}

let声明模仿上述IIFE所做的一切来简化循环过程,每次迭代循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。

全局块作用域绑定

当var被用于全局作用域时,它会创建一个新的全局变量作为全局对象的属性。这意味着用var很可能会无意中覆盖一个已经存在的全局属性;如果在全局作用域中使用let或const,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。即,用let或const不能覆盖全局变量,只能遮蔽它,相对比较安全。


我和我最后的倔强
161 声望4 粉丝

下一篇 »
es6--函数