闲话
学习ES6的动机起于对其promise标准的好奇,它与jQuery源码中Deferred不同,而且在异步编程中加入了Generator,在后续ES7中更有Async。这勾起我强烈的兴趣了解ES6更多的内容,于是完整的学习了阮一峰老师的《ECMAScript 6入门》。
本文不对规范细节做详细说明。希望通过这篇博客,记录自己所理解的es6的语言风格和编程思想。
注:以《ECMAScript 6入门》
为蓝本,大量用例出自其中。
ES6介绍
ECMAScript 6
(简称ES6)是JavaScript语言的下一代标准,于2015年6月正式发布,也称ECMAScript 2015。
摘自《ECMAScript 6入门》
ECMAScript 3.0(1999年12月)成为通行标准,奠定了JavaScript通行标准,直到今天,我们一开始学,都是在学3.0版本语法。
ECMAScript 4.0草案(2007年10月),对ES3做了彻底升级,各方代表对是否通过产生严重分歧。2008年7月,EMCA开会决定终止开发,将其中涉及现有功能改善的一小部分,发布为ECMAScript 3.1(会后不久改名为ECMAScript 5),将其他激进的设想放在以后的版本,由于会议的气氛,该项目代号起名为Harmony(和谐)。2009年12月,ECMAScript 5.0正式发布。Harmony项目一分为二,一些较为可行的设想定为JavaScript.next继续开发,后来演变成ES6,一些不是很成熟的设想,被视为JavaScript.next.next,在更远的将来再考虑推出。
2011年6月,ECMAscript 5.1版发布,并且成为ISO国际标准(ISO/IEC 16262:2011)
2015年6月,ECMAScript 6正式通过,成为国际标准。
ES6总览
tips:
ES6规范的原则是尽可能完整的向下兼容,除了块级作用域支持外,原有代码几乎不受影响。通过新增API及语法扩展支持。随着规范的普及,完全参照严格模式
'use strict'
将成为编程最佳实践不同类别的工具方法挂载在对应的构造函数上,而不是作为全局方法(如isNaN() -> Number.isNaN()),对原有全局方法进行了迁移(原有的还在)。
下面,分 5 点对 ES6 进行全面解读。ES6总览后,为每点的分条详述。
1、语法升级
对基本语法进行了增强,并调整为块级作用域支持。
用更直观的“声明式”思想(解构赋值、...
扩展运算符、无 this 上下文困扰的箭头函数、for…of 遍历),对取值、赋值、对象表示、构造函数及继承等的过程进行了大幅简化。
2、模块化
静态化的模块系统支持(默认严格模式编程)。完美的循环依赖处理(commonjs只算半支持),动态的输出值引用。
3、类型升级
Number 新的二/八进制写法、浮点误差、安全数;String RegExp:全面支持32位utf16字符,定义了超简易的模板字符串拼接(并可便捷的自定义模板处理规则);引入基本数据类型Symbol,代表独一无二值,有效防止属性命名冲突;Array数组空位处理方法的修正,提供 for…of 遍历及对名值遍历的API支持;新增数据结构Set
和Map
及弱引用的WeakSet
和WeakMap
,可去重存储value、key-value。
数据结构的增加,使得ES6 for…of遍历不仅仅需要对数组、字符串等带有length
属性的类数组生效,还需要能够个性化定制。抽象出Symbol.iterator
接口,凡是带有该接口的对象均可被遍历(仅有length属性的类数组不可以),调用该接口。比如数组会调用Array.prototypeSymbol.iterator。
Symbol
属性的添加也使对象枚举相关的API增加了几个(是否枚举Symbol、原型链、不可枚举属性)
tips:遍历与枚举的不同在于,遍历是对值(value)的,枚举是对键(key)的。遍历的顺序是Symbol.iterator
接口定义的(数组是0~n数字顺序);枚举是底层内部定义的(顺序:先数字排序、属性按时间排、Symbol按时间排),未开放权限
4、语言层面
分层的权限
为了便于理解,我把底层行为分为 规则层(基于对象,被遍历、被枚举、被正则匹配、被new、被转类型等)、属性配置层(基于属性,propertyDescriptor
)。
ES6的一大特点是,开放权限。姑且把我所理解的权限分为 5 类:原型链、调用栈、作用域链、对象规则层、属性配置层。
ES6函数严格模式执行时不再对调用栈引用,此时支持尾递归优化。作用域链引用不可开放,这是词法作用域安全性、隔离性的根本。开放了规则层自定义,使得开发者能够自定义一些对细部规则的反应。开放了原型链的访问,使得已有对象也能直接改变原型链引用,使更强大的继承容易做到(通常尽可能不用)。属性配置层到了ES5
就比较完善了。
ES6把规则层的部分行为抽象为一系列接口,涉及被正则匹配、被判断instanceof、被for…of遍历、数组是否可展开、构造器的返回对象和stringTag等。出于防止命名冲突的考虑,都使用Symbol值(独一无二),保存在内置的Symbol构造函数的属性上,共11个(很多并不是语言层面的重要规则操作,定位:偏个性化的需求 + 部分重要规则)
Object实例是js里的基础对象,包括函数都是由object衍生而来。它是一种基本的key-value式的数据结构。
- 1、对象下通过内置Symbol规则属性个性化定义特殊行为时如何反应。
- 2、每个属性的
value
,只是属性描述的一部分。Object.getOwnPropertyDescriptor(obj, pro)
可获取,设定是否可枚举、可定义、只读、是否为访问器(get、set)。
Proxy和Reflect
ES6新增Proxy
数据类型,可以通过new Proxy(obj, handler)
生成对象操作的代理,本质是一个拦截层,涉及增删查改属性值、设置原型链、属性配置、遍历枚举、环境绑定、new等等操作(部分内置Symbol不是对对象的主要操作,只是小的个性化补充,就不包含在内了)。
新增Reflect
,提供了所有与Proxy对应的语言默认操作方法,一一对应,目前有 14 个。
Reflect有着几乎所有对对象的重要操作,ES6以前跟语言相关的配置操作都在Object上,都迁移了过去,并且对设置型的API都以返回false表示设置失败,而不是抛出错误。以后语言内部相关的方法都将扩充到Reflect,Object上不一定会添加。
5、异步编程
传统的异步使用回调函数,函数以参数形式传入以待调用。复杂情况时,回调函数里可能也有异步逻辑,导致层层嵌套。而且还需要手动catch错误。
ES6推出了promise
标准。既能把每层的逻辑解耦分开,又有自动的机制catch错误。通过then串联起来要执行的逻辑。
ES6支持Generator
函数。它是语言层面的支持,用同步的方式来顺序书写异步代码,以yield
暂停。相较promise
有着更直观的控制流管理,“半协程”的实现,使得在yield进程的切换中仍然保留着调用栈,使得内部定义的 try…catch 总能捕捉到内部的错误,是完全意义上的同步式写法。虽然在promise的源码中利用词法作用域的特点也能解决。
但Generator只相当于一个状态机,声明式的定义了流程,还需要封装一个co
模块函数才能实现支持异步逻辑的自动流程处理。
ES7提供了Async
函数,是Generator的语法糖,调用时等同于被co函数加载执行的Generator函数。到此,异步编程算是得到了最佳实践。
语法升级
核心:用一目了然的方式,简化表达。定义ES6推荐的最佳编程实践。
1、作用域
ES6支持了块级作用域,{} 部分包裹的代码块具有独立的作用域,如if、for。新增let
(变量) const
(常量)定义变量,必须先定义后使用,不会变量提升,更不容易出错,填var
的坑。
{
let a = 5;
const b = 4; // 不能重新赋值或改变引用,但能改变引用对象内的属性
a = 3; // 3
b = 3; // error
}
console.log(a) // error
// 自执行函数 作用有 2 点:1.防止全局污染; 2.构造闭包保存变量状态
// 在只需 第1点 时,可以 { 代码 } 替代
函数声明可以在块级内声明 { }不再报错,只在块级作用域中变量提升。
if (true)
function a() {
} // error
// 正确版本,不能省略{}
if (true) {
function a() {
}
}
console.log(a); // error
2、取值、赋值、对象表达
对象简写
let b ='check';
let obj = {
a: 1,
b, // 等同 b: b ,即 b: 'check'
c(x, y) { return x+y }, // 等同 c: function(){}
get d() { return 2; }, // 设置 d 的 get 取值器函数
[Symbol('foo')]() {
return true}, // 设置 [Symbol('foo')] 的函数值
* e(x) { yield x; } // 设置 e 的Generator函数值
function test(x, y) {
return {x, y}; // {x: x, y: y}
}
一步到位 的赋值方法 —— 解构赋值 + 默认值,直观、高效。
let [a, [b, c]] = [3, 'str'];
// a=3, b='s', c='t' 数组型赋值:要求右侧值有Symbol.iterator接口,如数组、字符串
let {a, b=4, c=4, d: _d=5} = {a: 2, b: 3}; // 等同 let {a: a, b: b=4, c: c=4, d: _d=5} = {a: 2, b: 3};
// a=2, b=3, c=4(默认值), _d=5(默认值)
[x, y] = [y, x] // 交换赋值
/* ---- 优化示例 ---- */
// ES3,遇上一个 fun(args) 的API,args有7个可选属性接口,看得出么,一脸懵逼(゚Д゚≡゚Д゚)
function sb(args) {
return args.a + args.b * args.c;
}
// ES6
function sb({a, b, c}) {
return a + b * c; // 无参数时出错
}
sb({a:1, b:2, c:3}); // 7
// 不传参数时默认为 {a:0, b:0, c:0}
function sb({a, b, c} = {a:0, b:0, c:0}) {
return a + b * c;
}
sb(); // 7
sb({a:2}); // error, 不使用默认值,但b、c为undefined
// 属性默认值
// 无值参数取默认值{},无a、b、c参数,默认取0
function sb({a=0, b=0, c=0} = {}) {
// 等同 {a: a=0, b: b=0, c: c=0} = {}
return a + b * c;
}
sb(); // 7
引入“…”扩展运算符 到 数组环境(函数参数算数组环境),用于赋值(左侧=)为rest
参数(只可用于尾参数),用于取值则为扩展值(=右侧,需Symbol.iterator
接口支持)
/* 赋值,rest 参数 */
let [a ,b, ...c] = [1, 2, 3, 4, 5];
// a=1, b=2, c=[3, 4, 5]
let [a ,b, ...[c, d]] = [1, 2, 3];
// a=1, b=2, c=3, d=undefined
function t(a, ...arr) {
}
t(1,2,3) -> a=1, arr[1, 2]
/* 取值 */
let a = [...[1, 2], 3, ...'str'];
// [1, 2, 3, 's', 't', 'r']
/* 两者结合 —— 解决平常厌恶的只能apply传入相同参数的问题 */
function test(...args) {
// 赋值
return function _test() {
return fun(...args); // 取值,等同 fun.apply(this, args)
}
}
ES7提案 引入“…”扩展运算符 到 对象环境。用于赋值(左侧=)为rest参数(只可用于尾参数),用于取值则为扩展值(=右侧,只扩展自身的可枚举属性,等同Object.key(obj))
/* 赋值,rest 参数 */
let {a ,b, ...re} = {a:1, b:2, c:3, d:4, e:5};
// a=1, b=2, re={c:3, d:4, e:5]
let {...{x, y}} = {x:1, y:2};
// x=1, y=2
/* 取值 */
let a = {...[1, 2], gg:3, ...{x:4, y:5}};
// {'0':1, '1':2, gg:3, x:4, y:5}
ES7提案 ‘::’简化bind
、apply
、call
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
::console.log // 等同于 console::console.log
3、箭头函数
只相当于一个简单的 { } 块级代码段(选择性使用)。没有普通函数的能力:独立的 this 上下文(这有时候是坑的来源。箭头函数 bind 也无效)、arguments 参数、对调用栈的访问。不能用作Generator
状态机。
let a = (x) => x+2;
// '=>' 左侧的参数若是一个,可简写为 let a = x => x+2;
// '=>' 右侧 x+2 是 {return x+2;} 的简写,{}中包含函数中所有代码
// 若返回对象,可({})返回。(x) => {return {id: x};} 可简写为 x => ({id: x})
var obj = {
a: 1,
b: function() {
setTimeout( () => {
this.a++; // this 为 b 函数内 this,可以更简单的绑定环境
}, 0);
}
};
// 便捷易懂的管道
let f = (x=0) => (y=0<