谈谈let与const

let 命令

let命令用于声明变量,但是与传统var命令的不同之处在于拥有以下特性:

  1. 使用let命令声明的变量只在let命令所在的代码块内有效(我将之称为变量绑定);
  2. 不存在变量提升;
  3. 存在暂时性死区;
  4. 不允许重复声明;
  5. 在全局声明,但不是全局对象的属性;

下面依次对这几个特性进行解释说明,并尝试探讨这几个特性的意义所在。

01 变量绑定

众所周知,在ES5规范中,JavaScript只有两种作用域:全局作用域与词法作用域(又称函数作用域或局部作用域)。也就是说,JavaScript中的变量要么被暴露在外,所有人都可以对其进行修改,要么就要被封装在函数体内,这样变量中的值就只有在函数内才可以获取或修正(除非使用闭包)。在漫长的开发实践中,开发者们逐渐发现基于以上两种作用域的变量绑定模式存在以下问题:

  1. function 关键字的过度使用(块级作用域);

有时候,我们的初心仅仅是想要安静的声明一些变量以供我们在某段代码中使用,并且不想冒着变量泄露全局的风险(这使我们的变量有可能覆盖到其他人的变量,或者被后来的其他人覆盖掉我们的变量,同时我们也不想维护有关变量名的长长的文档,把他们都变成一个个“关键字”)。在ES5规范中,我们的最佳实践是使用IIFE函数解决这一问题:

(function() {
var a = 1;
})();

console.log(1); // err

但是承认吧,这样的写法其实并不优雅不是吗,毕竟函数是“可执行的代码块”,而IIFE函数的唯一目的只是为了创建一个词法作用域环境。
于是,在ES6规范中,引入了块级作用域这个概念,写法只是简单的使用‘{}’表示,于是原先我们的IIFE函数就可以被替换为:

{
    let a = 1;
};

console.log(a); // err

是不是优雅了很多?其实ES6规范包含了许多这样使我们的代码更加简洁,优雅的语法糖。有关块级作用域的其他知识,请见我的另一篇文章[谈谈块级作用域]。

而let关键字即是用来将变量”绑定“至所在的块级作用域,也就是说,在所在的块级作用域外,我们无法读取,修改使用let关键字声明的变量。

  1. var关键字的声明的变量具有”变量提升“效果,这导致了一些问题:

    var tmp = new Date();

    function foo() {
    console.log(tmp);
    if (false) {
    var tmp = 'hello world';
    }
    }

    foo(); // undefined

由于没有块级作用域,var关键字声明的tmp变量在代码初始化的过程中被提升至foo函数作用域顶端,覆盖了全局作用域的tmp变量,因此输出的tmp值为undefined,这显然不是我们想要的结果。而有了块级作用域后,tmp改用let关键字声明,该变量就会乖巧的待在自身的块级作用域内,console.log则可以正确的输出我们想要的结果。

  1. 用来计数的循环变量会泄露为全局变量

    var s = 'hello';

    for (var i = 0; i<s.length; i++ ) {
    console.log(s[i]);
    }

    console.log(i); // 5

在本段代码中,函数的实际解析过程是这样的:

var s = 'hello';
var i = 0;

while ( i<s.length) {
    console.log(s[i]);
    i++;
}

console.log(i); // 5

发现了吗,我们无意中创建了一个全局变量i,这样的结果可能是我们不想要的。
因此ES6使用了块级作用域与let关键字解决这个问题:

var s = 'hello';

for (let i = 0; i<s.length; i++) {
    console.log(s[i]);
}

console.log(i); // error

看到了吗,原先的变量i不复存在,怎么做到的,我认为上面代码被解析为如下代码:

var s = 'hello';

{
    let i = 0;
    while ( i<s.length) {
        console.log(s[i]);
        i++;
    }
}

console.log(i); // undefined

值得注意的是,let关键字声明的变量仅在每次循环中有效,即每循环一次,都会重新创建一个全新的块级作用域并且它享有自己的let变量,利用这个特点,我们可以实现以往通过闭包才能实现的效果:

var arr = [];

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

a[6](); // 6

02 不存在变量提升

let关键字声明的变量不存在var关键字声明的变量那样会出现”变量提升“现象。所有变量都需要先声明后使用,否则会报错(这也意味着使用typeof关键字不再安全)。

也许,变量提升使我们的代码能够更加灵活,但是这种灵活性却带来了很多潜在的问题,let关键字拒绝了这种灵活性,却使我们的代码更加健壮,规范。

先声明后使用啊,朋友,let如是说。

03 暂时性死区

我已经出生(存在)了,只是还没有长大(声明),等我长大(声明)之后,你才可以娶(获取)我。这就是暂时性锁区的含义,在块级作用域内,let关键字绑定的变量名在声明之前不可获取,赋值,修改。这看起来似乎更加合理,很奇怪ES5规范中为什么没有这样规定。

04 不允许重复声明

“嘿,你没必要给我取两次相同的名字!“如果你视图在一个块级作用域内多次使用let声明一个变量,你就能听到let关键字冲着屏幕外的你向你怒吼,是的,你真的没有必要那样做。醒醒吧。ES6规范增加这一条这是为了给你一个友善的忠告。

05 虽然在全局声明但不是全局对象的属性

全局对象是最顶层的对象,在浏览器环境值的是window对象,在Node.js中值的是global对象,在ES5中,全局对象的属性与全局变量是等价的。但在ES6中,使用let关键字, const命令,class命令声明的全局变量将不再属于全局对象的属性。

为什么这样做?道理很简单,为了保护变量不会被意外的修改,但随之而来的问题是,在全局声明的变量去了哪里?

let b = 1;
window.b; // undefined

const 命令

const命令用来声明常量,一旦声明,其值就不能改变。这也就意味着,const一旦声明常量,就必须立即初始化。

除此之外,const的作用域与let命令相同,也拥有let命令的五大特性(仅在所在代码块内有效,暂时性死区,不存在变量提升,不允许重复声明, 在全局内声明不是全局对象的属性)。

最后值得一提的是,就像变量存储引用类型值时,存储的实际上是一个指向内存地址的”指针“,const实际上也是如此,因此其”值不能改变“,也就意味着”指针“不能发生改变,也就是说,你仍然可以在const声明的对象上添加属性,因为并未改变其指针。

如果想要完全”冻结“一个引用类型值(不可以在对象上添加属性),你需要使用Object.freeze方法:

const obj = Object.freeze({});
obj.prop = 123; // doesn't work

除了冻结对象本身之外,要想实现”全面封冻“,你还需要连带对象的属性一并冻结:

var constantize = (obj) => {
    Object.freeze(obj);
    Object.keys(obj).foreach( (key, value) => {
        if (typeof obj[key] === 'object') {
            constantize(obj[key]);
        }
    });
};
posted @ 2017-05-27 14:42  libinfs  阅读(690)  评论(0编辑  收藏  举报