写在前面
let x = x => x + 1;
似乎成了ES6的起手式,如同return arr.map(fx).filter(isValid).reduce(accumulator)
作为ES5的亮黑色一样,会点ES6,还用var
起手会被人嫌弃的
不过,ES6中最不疼不痒的特性应该就是let
和const
了,如果已经习惯了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(明明应该是由一对花括号来搞定的事情)
于是就有了let
和const
二.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-of
、for-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中文站提供的免费电子书