【es6】你真的懂let和const吗?

662 阅读1分钟

今天IT交友群里聊到let和以及变量提升,就随便扯了扯。晚上想了想平时都一直在用let定义一些可变的变量,用const定义一些不可变的变量,以及没有变量提升,仅此而已。但是真正let和const只是这么点吗?当然不是,今天就深入的看一下。

一、基本用法

我们都知道let所申明的变量只在let命令所在的代码块内有效,这是let和var的一点重要区别。常见的例子就是for循环:

var a = [];
    for (var i = 0; i < 10; i++) {  
        a[i] = function () {    
        console.log(i);  
    };
}
a[6](); // 10

在上述代码中,i是全局的i,所以循环的每一次i都是变化的,不管循环多少次所有的i都是指向同一个i,所以最后数组a的成员都是同一个i,而这个i最后变成了10,所以a[6]就是10。  但是如果使用let,其声明的变量只在块级作用域内有效,如下:

var a = [];
    for (let i = 0; i < 10; i++) {  
        a[i] = function () {    
        console.log(i);  
    };
}
a[6](); // 6

每一次循环的i 都是只在本循环内有效,其实每一次循环的i 都是一个新的变量,所以最后a[6]=6。

Q: 如果每一轮的循环都是新声明的i,那么JS是怎么知道上一轮循环的i 的值的?

A:这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域,如下:

for (let i = 0; i < 3; i++) {  
    let i = 'abc';  
    console.log(i);
}
// abc
// abc
// abc

上述我们在内部也定义了i变量,并赋值为一个字符串,但是并不影响设置循环变量那部分的声明。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

二、不存在变量提升

var定义的变量存在一个变量提升大家都是知道的,但是let不存在变量提升。如果在声明之前使用该变量,不会是undefined,而是直接报错。

console.log(foo); // 输出undefined
var foo = 2;

console.log(bar); // 报错ReferenceError
let bar = 2;

三、暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”这个区域,不再受外部的影响。ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错, 这就是暂时性死区。eg.

var tmp = 123;
if (true) {  
    tmp = 'abc'; // ReferenceError  
    let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

四、不允许重复声明

不允许重复声明同一个变量,大家基本都知道,不过平时不怎么注意的是:入参也不能重复声明.

function func(arg) {  
    let arg;
}
func() // 报错   变量和入参重复

function func(arg) {  
    {    
        let arg;  
    }
}
func() // 不报错, 因为它又在内部一个{}内了

四、块级作用域

块级作用域是es6才有的,在es5只有全局作用域和函数作用域,这就带来很多不合理的现象,如下:

var tmp = new Date();
function f() {  
    console.log(tmp);  
    if (false) {    
        var tmp = 'hello world';  
    }
}
f(); // undefined

这里并没有打印出时间,虽然不会进入if条件语句内,但是tmp还是被改变了。 另外还有的就是上面提到的for循环,在for循环内部定义的变量无意中成为了全局变量,可能覆盖掉已经存在的数据。  上述代码改为使用let:

let a=1
function foo(){  
    console.log('a:', a)  // a: 1  
    if(false){    
        let a=2  
    }
}

五、块级作用域下的函数声明

function f() { console.log('I am outside!'); }

(function () {  
    if (false) {    
        // 重复声明一次函数f    
        function f() { 
            console.log('I am inside!'); 
        }
    }
    f();
}());// Uncaught TypeError: f is not a function

上述代码实际运行如下:

function f() { console.log('I am outside!'); }

(function () {  
    var f = undefined;  
    if (false) {    
        function f() { console.log('I am inside!'); }  
    }
    f();
}());

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

// 块级作用域内部的函数声明语句,建议不要使用
{  
    let a = 'secret';  
    function f() {    
        return a;  
    }
}

// 块级作用域内部,优先使用函数表达式
{  
    let a = 'secret';  
    let f = function () {    
        return a;  
    };
}

另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

const的本质

const的作用域与let命令相同:只在声明所在的块级作用域内有效。同时const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

const的本质:const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了,比如:

const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;  // 可以对foo本身进行修改
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-onl

如果真的想将对象冻结,应该使用Object.freeze方法。

上述来自阮一峰老师ES6书总结

欢迎关注我的公众号: