let和const_ES6笔记11

写在前面

let x = x => x + 1;似乎成了ES6的起手式,如同return arr.map(fx).filter(isValid).reduce(accumulator)作为ES5的亮黑色一样,会点ES6,还用var起手会被人嫌弃的

不过,ES6中最不疼不痒的特性应该就是letconst了,如果已经习惯了var的小脾气的话

一.为什么需要let和const?

因为var有一些小脾气,他们认为是函数作用域引发的“bug”,比如这个小小的诡异问题:

var x = 4;
(function() {
    console.log(x);     // undefined
    console.log(x + 1); // NaN
    var x = 1;
    // 因为var的提升特性,以上代码等价于
    // var x;
    // console.log(x);
    // console.log(x + 1);
    // x = 1;
})();

这个叫Hosting(提升),被强行扣上了黑锅:

谁让你提升的,弄出来一串诡异的undefined、NaN,都怪你

其实如果不特意节省变量名的话,很难遇到这个问题

而另一个问题所有JS玩家都遇到过,如下:

(function() {
    var arr = [1, 2, 3];
    for (var i = 0; i < arr.length; i++) {
        setTimeout(function() {
            // 因为50ms后外部循环结束了,i === arr.length = 3
            console.log(arr[i]);    // undefined x 3
        }, 50);
        // 修复
        // (function(i) {
        //     setTimeout(function() {
        //         console.log(arr[i]);    // 1 2 3
        //     });
        // })(i);
    }
})();

闭包持有的是外部作用域访问权限,而不是变量的值,50ms后去访问i,拿到的当然是3,这就是闭包的特性,其它函数式语言的闭包也是这样子,他们又说:

谁让你不合常理,循环体执行时的状态你怎么不给我存着,害我取到一堆undefined

JS无力辩解,心想自己确实有些地方做的不对:

  • 全局作用域中var声明的变量会成为global对象的属性

  • 没有块级作用域,辛苦大家用了20年IIFE(明明应该是由一对花括号来搞定的事情)

于是就有了letconst

二.let的特点

1.let声明的变量有块级作用域

没错,20年后,JS也有块级作用域了

for (let i = 0; i < 3; i++) {
    //...
}
console.log(i); // Uncaught ReferenceError: i is not defined

那么就有了一个问题,创建“块”最简单的方式是什么?如果还是IIFE,那又有什么区别呢?答案见后文,因为涉及JS语法小细节,不在此展开

2.let也有提升特性

直接把第一个示例代码中的关键var换成let试试:

var y = 4;
(function() {
    console.log(y);     // Uncaught ReferenceError: y is not defined
    console.log(y + 1);
    let y = 1;
    // let也有提升特性,以上代码不完全等价于
    // let y;
    // console.log(y);     // undefined
    // console.log(y + 1); // NaN
    // y = 1;
})();

这次直接报错了,外层的y = 4被屏蔽了,说明let y确实提升了一个块级变量y,报错则是因为执行到let行才会加载变量定义(第6个特点)

因为TDZ(见后文)的存在,未被注释部分并不完全等价于被注释掉的代码(上面会报错,而下面不报错)

3.异常会在当前行抛出

let有助于定位错误,除NaN之外异常会在当前行抛出,比如undefined,把NaN排除在外是因为:

let a;
console.log(a + 1); // NaN === undefined + 1

JS的弱类型机制不认为NaN算异常

其它异常会在当前行抛出,对比可见:

// let 当前行报错
(() => {
    x++;    // Uncaught ReferenceError: x is not defined
    [1, 2, 3][x][0];
    let x = 1;
})();
// var 当前行不报错
(() => {
    x++;
    [1, 2, 3][x][0];    // Uncaught TypeError: Cannot read property '0' of undefined
    var x = 1;
})();

明明是x++时候就跑偏了,只到引发其它错误时才报错,let成功避免了这种情况

P.S.上面的(() => {/* 新版IIFE */})();只是为了隔离影响,便于测试,let + class + ES6模块就是为了剔除IIFE,合理的ES6代码中不应该出现仅用于隔离一块作用域的IIFE

4.let声明的全局变量不是全局对象的属性

let b = 2;
console.log(window.b);  // undefined

不是说不需要变量命名空间了,script标签并没有隔离作用域的效果,window上的自定义属性少了,全局变量的问题仍在,至于配合ES6模块作用域,这似乎是非常遥远的事情

P.S.虽然webpack等构建工具支持ES6模块,但只有浏览器支持这种模块作用域才能解决全局变量的问题,到时候或许真的就不需要命名空间了

P.S.V8几个月前就号称100%支持ES6了,但ES6模块一直不支持,可能也不打算支持,因为ES6模块机制不适合浏览器环境,原因以后再细说

5.let声明的循环变量每次迭代都会重新绑定

也就是说循环体中的闭包保留了循环变量的值的副本,如下:

(function() {
    var arr = [1, 2, 3];
    for (let i = 0; i < arr.length; i++) {
        // 闭包保留了循环变量的值的副本
        setTimeout(function() {
            console.log(arr[i]);    // 1 2 3
        }, 50);
    }
})();

大家希望保存循环体执行时状态,那就依大家的意思,JS妥协了,但只退了一小步,仅仅对循环变量做了点hack,闭包的大原则不能乱(持有外部作用域的访问权)

注意:循环变量的意思是,适用于现有的三循环方式for-offor-in以及传统的用分号分隔的类C循环

6.执行到let行才会加载变量定义

在这之前使用该变量报错ReferenceError这段时间变量在作用域中,但尚未加载,位于TDZ(Temporal Dead Zone)中

let是故意的,这样既兼容Hosting,同时还能报错

7.let变量作用域是整块有效,而不是从声明处开始到块结尾有效

与C语言不同,算是块级Hosting,有一个很贴切的描述:

JavaScript中var声明的作用域像是Photoshop中的油漆桶工具,从声明处开始向前后两个方向扩散,直到触及函数边界才停止

let的Hosting方式与var没太大区别(只是边界变成了块边界),都是这种双向扩散的

8.重定义let变量会报错SyntaxError

会在词法解析阶段报错,而不是运行时报错,而且SyntaxError无法被try-catch捕获

var的“容错性”很强,如下:

var x = 2;
var x;
var x = x++;

嗯,x还是2,第二句被忽略了,第三句var忽略掉,赋值执行了,怎么写都不报错

let x = 2;
let x;  // Uncaught SyntaxError: Identifier 'x' has already been declared

这样的话,以后面试题都简单多了:)

9.class声明和let一样,同名类会报错SyntaxError

class出厂时就和let签了合作条款,遵循let式声明规则:

class A {}
class A {}  // Uncaught SyntaxError: Identifier 'A' has already been declared

三.const的特点

const与let类似,但const变量只读

特点:

  • 修改const变量应该报错SyntaxError,但Chrome47操作无效但不报错

  • const声明必须同时赋值,否则报错,但Chrome47不报错,值为undefined

注意:这两个约束在Chrome51中已经有了,现在会报错(不知道是哪次的更新,话说这些ES6笔记是16年1月份的事情,不小心目击了规范的约束力)

示例如下:

// 尝试修改const变量
const PI = Math.PI;
PI = 3; // Uncaught TypeError: Assignment to constant variable.
PI++;   // 同上
console.log(PI);    // 注释掉上两句会输出3.141592653589793

// 尝试声明时不赋值
const UNDEF;    // 词法检查阶段报错
                // Uncaught SyntaxError: Missing initializer in const declaration
console.log(UNDEF); // 前面报错了,到不了这

四.创建块最简单的方式

ES6有块级作用域了,意味着IIFE隔离作用域将成为历史,那么替代品是什么?

{
    let tip = '这是我的领地';
};  //!!! 千万千万注意这个不起眼的分号
console.log(tip);   // Uncaught ReferenceError: tip is not defined

{};比IIFE清爽多了,等等,末尾的分号是什么东西,有用吗?Java里的代码块明显不需要分号吧

注意:这个不起眼的分号是必不可少的,去掉就会报错,因为{}会被当作对象字面量解析,引发语法错误,Java确实不需要这个分号,因为没有对象字面量没有歧义,词法解析器不会懵

JS中的花括号

其实JS中有4中花括号,分别是:

// 1.对象字面量
{
    a: 1,
    b: 2
}
// 2.复合语句(一组代码,单语句可以省略花括号)
if (true) {
    console.log(1);
    console.log(2);
}
// 3.作为语法结构(花括号是语法结构,不能省略)
try {}
catch (ex) {}
// 4.label(也是代码分组,用来支持break、continue的跨层跳转)
label: {}

块和对象字面量有歧义,都是

{
    //...
}

JS会把这个东西当作表达式来解析,因此检查对象字面量语法,不对就报错。如果告诉JS这个东西应该当作块来解析,歧义自然就没了,两种方法:

// 1.分号强制语句(和逗号强制表达式一样)
{/* 我是一个块语句 */};
// 2.复合语句
{{/* 我是一个复合语句 */}}

所以另一种稍麻烦的创建块的方式就是:

// 一种可爱的方式
{{
    let tip = '这是我的领地';
}}
console.log(tip);   // Uncaught ReferenceError: tip is not defined

当然故意用label来创建块也是可以的,反正label一般没什么用,如下:

block: {
    let tip = '这是我的领地';
}
console.log(tip);   // Uncaught ReferenceError: tip is not defined

数数就有3种方式,可能还有更多待发现的,关于JS语法的更多信息请查看《JavaScript语言精髓与编程实践》

五.总结

let是更完美的var

let...作为ES6的起手式也没错,但let相关的东西不比var少,用好let也像用好var一样不容易

此外,let虽然是var的替代品,但并不意味着可以对着老代码做一遍全文替换,let限制更多,“容错性”自然不如var。但无论怎样,该过去的都将过去,var终会消失,所以,尝试接受let

参考资料

  • 《JavaScript语言精髓与编程实践》:非常不错的一本书,如果有耐心看完的话

  • 《ES6 in Depth》:InfoQ中文站提供的免费电子书

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code