学习ES6
的动机起于对其promise
标准的好奇,它与jQuery
源码中Deferred
不同,而且在异步编程中加入了Generator
,在后续ES7
中更有Async
。这勾起我强烈的兴趣了解ES6
更多的内容,于是完整的学习了阮一峰老师的《ECMAScript 6入门》
。
本文不是专门的es6
笔记,因此不对规范细节做详细说明。希望通过这篇博客,记录自己所理解的es6
的语言风格和编程思想。
注:以《ECMAScript 6入门》
为蓝本,大量用例出自其中。
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
正式通过,成为国际标准。
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.prototype[Symbol.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
式的数据结构。
Symbol
规则属性个性化定义特殊行为时如何反应。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
推荐的最佳编程实践。
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
对象简写
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}
/* 两者结合 —— 解决平常厌恶的只能apply传入相同参数的问题 */
function test(...args) { // 赋值
return function _test() {
return fun(...args); // 取值,等同 fun.apply(this, args)
}
}
ES7
提案 ‘::’简化bind
、apply
、call
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
::console.log // 等同于 console::console.log
只相当于一个简单的{ }
块级代码段(选择性使用)。没有普通函数的能力:独立的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) => ({
before: (z=0, w=0) => x + y * z - w,
after: (z=0, w=0) => x * y - z - w
});
f(1)(1).before(1, 1); // 1
替代传统构造函数。不会变量提升。static
代表静态,其他为原型方法。在内部定义静态属性无效,结尾无分号
class A {
constructor(x, y) { // 若省略,则默认 constructor() {}
this.x = x;
this.y = y;
}
add() { // 实例方法
return x + y;
}
get z() { // 取值函数,obj.z = true;
return true;
}
static classMethod(obj) { // 静态方法,A.static
obj.x = 0;
obj.y = 0;
}
}
A.staticProp = 1;
ES7
提案 新增实例属性、静态属性值默认值定义。prop = 1;
static staticProp = 2;
带分号
class A {
constructor() { // 若省略,则默认 constructor() {}
this.x = 1;
}
}
A.staticProp = 2;
// 等同, ES6 暂不支持
class A {
x = 1;
static classMethod = 2;
}
extends
继承,能够继承static
属性、原型属性。对实例属性的继承不再通过转移环境A.apply(this, arguments)
,这样无法完整的继承内部构造函数(如Array
实例继承后没有动态变化的length
)。直接通过创建原函数实例,在该实例上修改,并设置原型的方式实现完整继承。
class A {
constructor(x, y) {
this.x = x;
this.y = y;
this.bool = new.target === A; // new.target 指向构造函数
}
prop() { return 1; }
static staticProp() { return 2; }
}
// extends 后可跟函数、返回函数的表达式
class B extends A {
constructor(x, y, z) { // 省略,则 constructor(...args) { super(...args); }
super(x, y); // 调用 A(x, y), 之后才可以 this.xxx 进行赋值
this.z = z;
}
prop() { // super 此处指父类实例
return 10 + super.prop();
}
static staticProp() { // super 此处指父类
return 10 + super.staticProp();
}
}
new A(1,2).bool // true
new B(1,2).bool // false , new.target 指向了B
ES6
新增模块(module)体系,有效解决命名冲突、复杂依赖的历史问题。模块内默认严格模式'use strict'
与commonjs
规范一样可以支持循环依赖(严格说commonjs
只能算一半支持,ES6
规范完全支持),但却是更底层的,静态化的处理,使得编译时就能确定模块的依赖关系,以及输入和输出的变量(引入的变量为动态引用,会一直随着源模块变量的值的变化而变化)。
export 输出、import 引出、export default 默认输出项,as 定义别名,* 用于 import 代表除默认项外所有
// 输出通常不这样写,除了 export default
export let a = 'str';
export function b() {};
// a.js
let a = 1, b = 2, c = 3;
export {a, b as aliasB}; // 以别名输出
export default c; // default 是特殊别名,一个模块只能一个
// 等同 export {a, b as aliasB, c as default};
// b.js
import def, {a, aliasB} from './a';
// 等同 import {a, aliasB, default as def} from './a';
// c.js
import * as mou from './a';
// mou.a = 1, mou.aliasB =2 。 * 内不包括 export default 的值,需单独引入
export * from './a'; // 继承,直接引入并全部传出
1、处理数值的方法迁移到Number
函数上,使语言结构更清晰。如全局方法isFinite()
、isNaN()
、parseInt()
、parseFloat()
,其中判断数值的Number.isFinite()
、Number.isNaN()
若参数不为数值、布尔值,直接返回false
2、在全语言的支持上,认为 +0
-0
不同,NaN
与自身相同(ES6
向后兼容,但在新增API中判断值的相等性时支持)
-2^53
到
2^53
之间的数无法正确表示、运算
1、优化了二进制、八进制的表示,分别为0b/0B
、0o/0O
,十六进制不变0x/0X
3 === 0b11 // true
9 === 0o11 // true
17 === 0x11 // true
2、新增极小常量(误差上限)、新增安全数判断(判断是否支持)
Number.isFinite(15) // true
Number.isNaN(3) // false, Number.isNaN('3')为true, isNaN('3')为false
Number.parseInt('123.45#') // '123', 行为不变
Number.parseFloat('123.45#') // '123.45', 行为不变
Number.EPSILON // 2.220446049250313e-16
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_SAFE_INTEGER // -9007199254740991
Number.isInteger(15.0) // true
Number.isSafeInteger(9007199254740992) // false
// 按照ES6的思想构造一个判断两数值是否相等的函数,ES6中 Object.is(A,B) 能辨别所有类型的A、B是否相等
function equalNum(A, B) {
if (Math.abs(A) > Number.MAX_SAFE_INTEGER) {
throw new Error(`${A} is outof safe range!`);
} else if (Math.abs(B) > Number.MAX_SAFE_INTEGER) {
throw new Error(`${B} is outof safe range!`);
} else if (Math.abs(A - B) < Number.EPSILON) {
return A !== 0 || 1/A === 1/B;
} else if (A !== A && B !== B) {
return true; // NaN
}
}
tips:新增若干Math
方法,对指数运算、三角函数提供更多支持。Math.trunc()
可对数值截取整数
1、从语言层面,让模板字符串的构建和解析更直观,一目了然,一步到位
let str;
str = 'I am me.You are you.';
// 等同于
const me = 'me', you = 'you';
str = `I am ${me}.You are ${you}.`;
反引号(`)中包裹的${param}
用来表示变量的值,是如下写法的缩写
let str = tag`I am ${me}.You are ${you}.`;
function tag(stringArr, value1, value2) {
// stringArr -> ['I am ', 'You are ', '.']
// stringArr.raw 指向前者中'\'被转义后的数组,若'\'已被转义,不做处理
// value1 -> 'me'
// value2 -> 'you'
let str = stringArr[0];
for (let i=1, len=arguments.length; i<len; i++) {
str += arguments[i] + stringArr[i];
}
return str;
}
通过显式的指定函数,可以轻松完成字符串的安全处理、模板解析等
let str = safeHtml`I am ${me}.You are ${you}.`;
// safeHtml函数 略
String.raw()
可以返回一个被转义的字符串,首参数为有raw
属性的数组、类数组
let str = String.raw`I am ${me}.\nYou are ${you}.`;
// 'I am me.\\nYou are you.'
// 等同于
let str = tag`I am ${me}.\nYou are ${you}.`;
function tag(stringArr, value1, value2) {
let _stringArr = stringArr.raw,
str = _stringArr[0];
for (let i=1, len=arguments.length; i<len; i++) {
str += arguments[i] + _stringArr[i];
}
return str;
}
2、通过新增API及写法,全面支持32位utf16
字符,同时向后兼容(关键:写法、length、遍历、匹配(通过RegExp支持))
/* 1. \u{}的全新写法,支持超过FFFF */
let s = "\uD842\uDFB7"; // markdown乱码了┑( ̄Д  ̄)┍,这货是一个字
// 等同于
let s = "\u{20BB7}";
/** * 2. 全新方法取字符编码 String.prototype.codePointAt()、把字符编码转字符 String.fromCodePoint() * 其实就是想了个新词 codePoint 替代 charCode,我会乱说吗 ┑( ̄Д  ̄)┍ */
"\uD842\uDFB7人".codePointAt(0) // 134071, charCodeAt为55362
"\uD842\uDFB7人".codePointAt(1) // 57271, 跟charCodeAt相同,length向下兼容,未调整
for (let a of "\uD842\uDFB7人") // ES6新增遍历,自然是全面支持32位字符的 -> '\uD842\uDFB7','人'
[..."\uD842\uDFB7人"].length // 2, 变通方法
// tips: 老方法 "\uD842\uDFB7人加"charAt(2) -> "加", ES6没有对应API,目前ES7有一个提案,用String.prototype.at() 替代。可通过[...str](pos)取值
3、新增字符串的String.prototype.repeat()
重复、String.prototype.padStart()/padEnd()
补位(未增加覆盖功能,如数组新增的copyWithin
)
'he'.repeat(3) // 'hehehe'
'he'.padStart(9, 'ab') // 'abababahe', 若(1, 'ab') -> 'he'
'he'.padEnd(9, 'ab') // 'heabababa'
tips:
新增String.prototype.includes(str)/startWith(str, pos)/endWith(str, pos)
。由于增有concat()
+
、删改有replace()
、查有indexOf()
search()
、截取有splice()
、打断成数组split()
,而新方法并没有在length
和查询匹配上做修正以支持32
位utf16
字符(不理解┑( ̄Д  ̄)┍),因此看起来倒不是很必要
ES6
的思路是把32
位utf16
字符匹配,放在RegExp
有关的方法上,新增全新的flag模式
正则的作用是快速匹配字符串
1、ES6
把所有跟正则有关的核心代码,迁移到了RegExp.prototype
String.prototype.match 调用 RegExp.prototype[Symbol.match]
String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
String.prototype.search 调用 RegExp.prototype[Symbol.search]
String.prototype.split 调用 RegExp.prototype[Symbol.split]
2、新增flag
修饰符u
(开启32位编码查询支持)、 y
(粘连式全局匹配)。原有g
全局匹配、i
忽略大小写、m
支持多行查找
/* 开启 32位UTF16字符 识别 */
/\uD83D/.test('\uD83D\uDC2A') // true
/\uD83D/u.test('\uD83D\uDC2A') // false, 能正确识别右侧为一个字
/^.$/.test('\uD83D\uDC2A') // false, 解读成2个字符
/^.$/u.test('\uD83D\uDC2A') // true
/* 粘连全局匹配,有时易于发现非法字符 */
'aa_a_ba_'.match(/a+_/g) // ['aa_', 'a_', 'a_']
'aa_a_ba_'.match(/a+_/y) // ['aa_', 'a_'], 顺序全局匹配,一旦不符,返回
'#x#2'.split(/#/y) // ['', 'x#2']
'aaxa'.replace(/a/y, '-') // '--xa'
/a/y.sticky // true
*3、ES7
提案:后行断言(之前只有先行断言支持)。断言可以不捕获,只匹配不包含断言的部分
/* 先行断言lookahead */
/x(?=y)/
/x(?!y)/
/* 后行断言lookbehind */
/(?=x)y)/
/(?!x)y/
tips:
新增RegExp.escape()
,用于双重转义字符串中的’\’,可用于new RegExp(RegExp.escape(str), flags)
生成正则。/ab\nf/.source
也会输出双重转义的字符串,可用于new RegExp()
在正则匹配失败的时候,经常会性能糟糕。因为正则中通常的贪婪或吝啬匹配,都是在能匹配成功的情况下的。当不成功时,就会回溯,若正则复杂叠加了多层,就是性能灾难。因此对于确定匹配的项目,可以使用 断言+反向引用
Symbol
是ES6
新增的基本数据类型,用来指定独一无二值。
出现原因:
对象的属性使用字符串指定,但是很可能会跟原有的属性名一致,或者后来者造成属性覆盖。以往通常是用 字符串+Date毫秒(或随机数)。为了解决这个头疼的问题,引入了独一无二值数据类型Symbol
。再也不用绞尽脑汁的想奇奇怪怪的名字了。。┑( ̄Д  ̄)┍
Symbol()
、Symbol.for()
、Symbol.keyFor()
// Symbol(key) 每一个都不相同,不会注册到全局,不能被Symbol.for()使用
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
Symbol.keyFor(s2); // undefined,未注册,无法搜寻
// Symbol.for(key) 先搜寻全局寻找key对应的Symbol,若无,生成一个Symbol并注册到全局
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // true
Symbol.keyFor(s2); // 'foo'
ES6
提供了11个内置Symbol
值,指向语言内部使用的方法。之所以如此,是为了防止使用时人为的命名冲突,得用未注册到全局的Symbol
值,必须把属性名保存起来
Symbol.iterator // 最常用。带有该接口,才能被 for...of 、...遍历
// 如下,通常有才调用,没有则默认行为
Symbol.hasInstance // a instanceof MyClass,调用MyClass[Symbol.hasInstance](foo)
Symbol.isConcatSpreadable // 使用 Array.prototype.concat() 时是否可被展开
Symbol.species // 作为构造函数时,返回值
Symbol.match // 被使用 String.prototype.match() 时
Symbol.replace
Symbol.search
Symbol.split
Symbol.toPrimitive // 被转为原始类型值时 obj[Symbol.toPrimitive](type)
Symbol.toStringTag // "[object xxx]" 修改xxx部分的字符串
Symbol.unscopables // 指定使用with时,哪些属性被排除 { propA: true }
我们经常用for (var i=0; i<len; i++)
的方式进行遍历(如数组)。ES5
数组实例支持filter
、map
等方法,使得基于遍历的处理变得更简单。其实这就是迭代器,但是ES5
的迭代API
都会跳过数组空位,与ES6
的思想不符,需要新的方法支持。
这里说说迭代器本身。迭代器分为两种:内部迭代器、外部迭代器。内部迭代器逻辑简单却不够灵活,外部迭代器稍微复杂,但足够灵巧。
// 内部迭代器 - 示例
Array.prototype.mapDemo = function(callback) {
for (let i=0; i<this.length; i++) {
callback(this[i], i);
}
}
// 外部迭代器 - 示例
var Iterator = function(obj) {
let current = 0;
return {
next() {
return current < obj.length ?
{value: obj[current++], done: false} :
{value: undefined, done: true};
}
}
};
ES6
新增了Set
Map
数据结构,为了给不同数据结构提供一个统一的访问机制,并且更灵活的迭代。抽象出了Iterator
(遍历器),能够细粒度的访问元素,同时for...of
提供自动的遍历。所有对象必须有[Symbol.iterator]
属性,才能够被for...of
遍历。=
右侧的...
扩展运算符用于数组时,也会调用for...of
。
对象的[Symbol.iterator]
属性被调用得到Iterator
对象(在ES6
的实现里Iterator
对象的__proto__
原型都会是一个有[Symbol.iterator]
接口的对象,执行anIterator[Symbol.iterator]()
会返回自身,因此Iterator
对象本身也可以被遍历,自己添加[Symbol.iterator]
执行后返回的遍历器自然就没有这个待遇了),Iterator
对象调用next
要求返回{value: contentHere, done: boolean}
带有value
和done
的接口(见外部迭代器代码)。当done
为true
时表示结束(该项value
不被计入)。使用for (let x of anIterator)
,则x
为每项的value
。
for...of
是对值的遍历(不是对index/key
,能正确遍历带32位utf16字符的字符串),若中途提前退出(通常是因为出错,或者有break
语句或continue
语句),将触发Iterator
对象的return
方法(与throw
方法是可选配置),必须返回一个对象,如{done: true}
。
for (let [x, y] of [[1,2], [3,4]]) {
console.log(x, y);
}
// 1 2
// 3 4
// `Iterator`对象本身也可以被 for...of 遍历
for (let [x, y] of [[1,2], [3,4]][Symbol.iterator]()) {
console.log(x, y);
}
ES6
新增了Symbol
类型值可以作为对象属性,for...in
不能枚举Symbol
属性,提供了新的API支持。(枚举是对值key
的遍历,不是value
。枚举相当于对遍历的一种加工)
(1) for...in
遍历对象自身和继承(__proto__)的可枚举属性(不含Symbol属性)
/* 下面均返回可遍历的对象 */
(2) Object.keys(obj)
返回数组,包括对象自身的可枚举属性(不含Symbol属性)
(3) Object.getOwnPropertyNames(obj)
返回数组,包含对象自身的所有属性(不含Symbol属性)
(4) Object.getOwnPropertySymbols(obj)
返回数组,包含对象自身的Symbol属性
(5) Reflect.ownKeys(obj)
返回数组,包含自身所有属性
(6) Reflect.enumerate(obj)
返回Iterator对象,对其let...of遍历,会与for (x in obj) 表现一致
老问题:
Array
构造数组时单参数和多参数行为不一ES5 API
在操作时直接跳过,ES6
改变了策略,新API
都当做undefined
处理 新增Array.of()
,与new Array()
多参数时一致
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1, 2, 3) // [1, 2, 3]
new Array(4) // [,,,,]
new Array(1, 2) // [1, 2]
“语法升级”中提到...
扩展运算符,可以把带有Symbol.iterator
属性的对象转为数组。在整个ES6
体系里已经抛弃了对类数组(带length
)的眷顾(因为遍历被进一步抽象,用于不只是数值索引的情况),但是它可以! - > Array.from(obj, map)
,同时支持类数组。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.from([1,2]); // [1,2] 一模一样的新数组
Array.from({ length: 3 }); // [ undefined, undefined, undefinded ]
Array.from({ length: 3 }, (value, index) => index); // [0, 1, 2],阿里面试题答案有木有!
// 类数组如下方式可以支持`for...of`遍历
arrayLike[Symbol.iterator] = Array.prototype[Symbol.iterator];
[...arrayLike]
ES7
新增了一种数组推导的方式从现有数组生成新数组,比Array.from()
强大,非常简洁!!可以替代map
和filter
方法。
let years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];
[for (year of years) if (year > 2000) if(year < 2010) year];
// [ 2006]
Array.prototype.keys()/values()/entries()
分别返回名、值、名值对的遍历器对象。使得只对value
遍历的for...of
能够对数组完成多种遍历
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let value of ['a', 'b'].values()) {
console.log(value);
}
// 'a'
// 'b'
for (let [index, value] of ['a', 'b'].entries()) {
console.log(index, value);
}
// 0 'a'
// 1 'b'
新增Array.prototype.copyWithin(target, start, end)/fill(value)
,表示 移位覆盖/填充
Array.prototype.copyWithin(target, start, end)
target(可选) -> 从该位置开始替换数据
start(可选) -> 从该位置开始读取,默认0,负数表示倒数
end(可选) -> 到该位置前停止读取数据,默认等于length,负数表示倒数
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
[1, 2, 3].fill(7)
// [7, 7, 7]
新增Array.prototype.includes(value)/find(value, index, arr)/findIndex(value, index, arr)
,取代indexOf()
,可判断NaN
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
对象的简洁表示,已经在”语法升级”中做了说明。
ES6
新增Objet.assign()
,用于对象可枚举属性合并(浅拷贝,一层)。类似于jq
的extend
。
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
新增Object.is(A, B)
,用来判断两个值或对象是否相等。但能正确判断NaN
,+0
和-0
不等。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
ES6
开放了原型链的设置权限。用可访问的__proto__
属性存取对象原型链(不建议),也可用Object.setPrototypeOf(),Object.getPrototypeOf()
方法存取(建议)。
var obj = {
method: function() { ... },
__proto__ : someOtherObj
}
Object.getPrototypeOf(obj); // someOtherObj
Object.setPrototypeOf(obj, anOtherObj);
对象没有[Symbol.iterator]
接口,不能直接实现对value
的遍历,可以通过Object.keys()
得到数组集合,然后通过obj[prop]
求得值。
ES7
提案,参考数组,引入与Object.keys()
配套的Object.values()
Object.entries()
,返回数组。三个API都只对自身可枚举的非Symbol
属性有效。
ES7
提案,新增Object.getOwnPropertyDescriptors()
(所有自身属性),与ES5
方法Object.getOwnPropertyDescriptor()
配套。若加入标准,则会有相应的Reflect.getOwnPropertyDescriptors()
方法。
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
ES6
新增数据结构Set
Map
,和与之配套的WeakSet
WeakMap
。但只有Set
Map
有[Symbol.iterator]
接口,能被遍历。
Set
可以看做对数组不能剔除重复项的补充(自动去重),Map
可以看做对对象只能以字符串为键的补充(还可以以对象为键)。
/* Set 和 Map 公有属性方法*/
// size 成员数
// add(value)/Set(key, value) 添加,返回Set/Map对象本身
// delete(xx) 删除,返回布尔值,表示删除是否成功
// has(xx) 返回布尔值
// clear() 清空,无返回值
// Map专有方法:get(key) 获取对应value
/* Set */
let set = new Set();
set.add({}).add({}); // 等同 new Set([{}, {}]);
set.size // 2
set.add(1).add(1);
set.size // 3,不会重复添加1
/* Map */
let map = new Map();
map.set(NaN, '111').set({}, '222'); // 等同 new Map([[NaN, '111'], [{}, '222']]);
map.get(NaN) // '111',若key为简单类型值,除NaN外,只要===,将其视为一个键,NaN也都视为一个键
Set
和Map
不是数组,没有数字索引,需要遍历支持。提供了4个公有方法
keys():返回一个键名的遍历器
values():返回一个键值的遍历器
entries():返回一个键值对的遍历器
forEach():使用回调函数遍历每个成员
// 使用示例
let map = new Map();
// some code...
for (let a of map.entries()) {}
WeakSet
和WeakMap
是ES6
提供的两种弱引用类型。分别只支持value为对象/key为对象
的情形。与Set
和Map
不同,储存的数据如果在环境中不再被引用,则会被垃圾清理机制,WeakSet
和WeakMap
并不会引用到它们。没有size
、clear()
和4个遍历接口。
这让我有一个简单的猜测,每个WeakSet/WeakMap
对象创建的时候,内部产生一个独一无二的Symbol()
值。对其add/set
时,对value/key
添加Symbol()
属性,这样被调用has(obj)
时直接根据obj
是否有它内部的Symbol()
值判断是否包含在内。这样就根本不存在引用了。但是对于WeakMap
,key
对象的Symbol()
属性上需要保存value
值,符合规范中说的key
对value
引用,但WeakMap
不引用key
。这也吻合为什么储存键必须要对象类型,而且没有size
、clear()
,且不支持遍历接口的说法。(但是我在chrome
试了一下,但是并没有发现多出来Symbol
类型属性。装逼失败,就当我什么都没说吧 2333333)。
“类型升级”一节中对内置Symbol
和Object
原型链等进行了说明。
新增语言拦截层代理对象类型Proxy
,用来对操作对象的底层行为进行拦截,指定调用的操作。同时新增对象Reflect
,上面挂载着几乎所有与操作对象行为有关的底层活动的默认方法。以后新增的与语言底层行为有关的API也将挂载在Reflect
上,而不是Object
上。Reflect
对设置失败的情况,返回false
,之前的Object
上是抛出错误
Proxy
与Reflect
的支持操作是一一对应的。使用方式new Proxy(target, handler)
,handler
为一个对象,可以指定下述14种拦截函数。
(1)get(target, propKey, receiver) // 拦截对象属性读取
(2)set(target, propKey, value, receiver) // 拦截对象属性设置
(3)has(target, propKey) // 拦截 xx in obj 操作,返回布尔值
(4)deleteProperty(target, propKey) // 拦截删除属性操作,返回布尔值
(5)enumerate(target) // 拦截 for...in 枚举,返回一个遍历器
(6)ownKeys(target) // 拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组
(7)apply(target, object, args) // 拦截Proxy实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)
(8)construct(target, args, proxy) // 拦截Proxy实例作为构造函数调用的操作,比如new proxy(...args)
(9)getOwnPropertyDescriptor(target, propKey) // 拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
(10)defineProperty(target, propKey, propDesc) // 拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值
(11)getPrototypeOf(target) // 拦截 Object.getPrototypeOf(proxy),返回一个对象
(12)setPrototypeOf(target, proto) // 拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值
(13)preventExtensions(target) // 拦截 Object.preventExtensions(proxy),返回一个布尔值
(14)isExtensible(target) // 拦截 Object.isExtensible(proxy),返回一个布尔值
/* 使用方式 */
let Person = class {
constructor(name) {
this.name = name;
}
static getAge() {
return 30;
}
isMan() {
return true;
}
};
// proxy成了Person的代理
let proxy = new Proxy(Person, {
get(target, propKey, receiver) {
return function() {
return 40;
}
},
construct(target, args, proxy) {
return 3;
},
apply(target, object, args) {
Reflect.apply(target, object, args); // 调用默认,原模原样不改变
}
});
proxy.getAge(); // 40
let param = new proxy('张三'); // 3
ES6
以前,异步编程通常使用回调函数的方式实现。层层嵌套,让人深恶痛绝,并且每层都要手动捕捉错误,很多外部库通过promise
对象来降低代码的耦合性。jQuery
的Deferred
对象是其中一种非标准的实现。
promise
的实现依赖于观察者模式,我们先通过简单的玩具代码了解下思路。(也可以跳过直接看下文)
function Watch() {
let cache = [],
memory, // 若触发过resolve,会记录参数
self = {
add(callback) {
if (memory === undefined)
cache.push(callback);
else
callback(...memory);
return this;
},
resolve(...args) {
memory = args;
for (let i=0, fn; i<cache.length; i++) {
fn = cache.shift();
fn(...args);
}
}
};
return self;
}
function test() {
// do something
let watch = Wacth();
某个事件 = function(...args) {
watch.resolve(...args);
};
return watch;
}
test().add(function(){...}).add(function(){...});
上面的代码可以看到,通过一个堆栈对象的传递,可以完成回调函数的分离。但是有两个明显的缺点:
1、暴露了resolve
接口,外部也可手动触发
2、add
的多个回调函数之间是同步执行的,不能异步等待
对上述问题,可以通过参数注入触发resolve
,使用Watch(test(resolve){...})
的形式编程,无需返回resolve
接口。可以提供接口then
替代add
,then
方法内部每次返回一个全新的Watch
对象,当then(回调A)
时,在原Watch
对象上add
function(){ 回调A(); resolve新Wactch; }
,从而实现链式的回调绑定(无论同步异步)。
function Watch(fun) {
let cache = [],
memory,
add = function(callback) {
if (memory === undefined)
cache.push(callback);
else
callback(...memory);
return this;
},
then = function(fun) {
// 返回新的 {then: then} 对象
return Watch(function(resolve) {
add(function(...args) {
let res = fun(...args);
if (res === undefined) {
resolve(...args);
} else if (res && typeof res.then === 'function') {
res.then(function(...args){
return resolve(...args);
});
} else {
resolve(res);
}
});
});
},
resolve = function(...args) {
memory = args;
for (let i=0, fn; i<cache.length; i++) {
fn = cache.shift();
fn(...args);
}
};
fun(resolve);
return {then};
}
Watch(function(resolve) {
// do something
某个事件 = function(...args) {
resolve(...args);
};
})
.then(function(){...})
.then(function(){...});
以上是promise
思想的雏形,下面说说ES6
的promise
规范。
ES6
新增Promise
类型,使用面向对象式的方式构造,需要用new
操作符调用,返回一个promise
对象。内部有三种状态:Pending(进行中)
Resolved(已完成)
Rejected(已失败)
。当成功或失败触发后,状态被冻结。
Promise
有Promise.prototype.then(successFun, errorFun)
接口,返回的是一个新的Promise
实例(不是原Promise
实例,见上面的玩具代码),可以链式绑定回调函数。若成功则触发前者,失败触发后者,无论前者还是后者,顺利执行后都继续触发下一个then
的successFun
(但当then(fn, null)
或then(null, fn)
触发到null/undefined
的情况,则以相同的状态和参数触发下一个then
),若执行过程中抛出错误,则触发下一个then
的errorFun
。
若回调函数无返回值,参数一直原样传递到下一个回调,执行出错,则传递error
对象。若回调返回promise
对象,则等待该promise
对象来调用then
返回的promise
。若返回其他值,则改变需要传递的参数为返回值给下一个回调。
Promise.prototype.catch(errorFun)
内部调用的then(null, errorFun)
。
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(succFun1).then(successFun2).catch(errorFun);
在jQuery
的Deferred
实现中,除了then
还有done/fail
接口(等同玩具代码中的add
)。then
的好处之一是支持回调函数间存在异步的时候仍然能顺序调用。坏处是会增加很多promise
对象的开销,很多时候then
中的函数也并不会返回promise
对象。为什么不实现done/fail
接口,让我们可以done
fail
和then
按照需要混合来用呢?我认为还有一个重要的原因是关于异常抛出。
异常抛出的顺序是:当new Promise(fun)
中fun
的执行中出现错误,则触发该promise
的reject
。当该promise
内部堆栈中的回调被触发时,若执行中抛出错误,触发下一个promise
的reject
(如果有)。但是done
接口添加的方法,如果出错,在源码中只能实现触发该promise
的reject
,而该promise
的状态应该是被锁定的,这样就没法把状态传递到后面的promise
上了。then
接口之所以能做到,是因为then
中的回调是被添加到调用它的promise
上,而这个逻辑是写在then
中的新生成promise
对象中的,可以调用到它的resolve
和reject
(词法作用域,可参照玩具代码)。
玩具代码中没有写关于异常抛出的代码,这里提一个注意点:记住,ECMAScript是单线程语言,只能保持一个调用栈,属于传统的子例程,它的执行上下文是采用后进先出的形式,上层上下文(子函数)完全执行完毕会被pop
,才会继续执行下层上下文(父函数)。所以使用事件触发机制执行resolve/reject
时,try...catch
通常是捕捉不到的,错误会由当前上下文一直向下层传递,直到被捕获。因此在回调的异常在then
中封装回调的函数中,回调执行的代码外层捕捉。
《ECMAScript 6 入门》
中只提到了then
、catch
实例方法,我写了一个只暴露这两个接口的实现(不暴露任何属性),用了170行,包括Promise
工具方法和完整的错误捕捉。前文已经提到介绍了,不暴露resolve
和reject
对具体实现的影响。由于规范是用构造函数的方式而非闭包,因此Promise
的原型方法并不能调用构造器里的私有变量。在玩具代码的实现中,memory
和cache
是必要的暴露项(通过this.cache
),或者把它抽象成一个如jq
中callback
一样的观察者模式,暴露出done/fail
接口。否则只能把then
方法写进构造器里this
的属性上,就可以使用到私有变量。(所以如果没有语言层面的特殊处理,怀疑应该是有其他暴露项的,否则就是then
以实例的属性形式返回)
(以下关于Promise.resolve/reject(obj)
的说法有待考证,书中的信息量不足,在参数为promise
对象和thenable
对象时,Promise.resolve
遇上了失败、Promise.reject
遇上了成功会如何表现没有提到。从后面Promise.all/race()
的表现看,个人觉得,当参数为promise
对象和thenable
对象时,由该对象状态决定触发成功和失败状态更合理,而参数为其他值时,则触发两方法对应的默认状态。)
Promise.resolve(obj)
用于将现有对象转为promise
对象。当参数是promise
对象时,直接返回该对象;当参数是带有then
方法的对象时,包装成promise
对象,并用obj.then()
触发该promise
的成功或失败状态;当参数为其他值时或无参数时,返回一个被resolve(obj)
的promise
对象。
let jsPromise = Promise.resolve($.ajax('/whatever.json'));
jsPromise.then(successFun).catch(errorFun);
Promise.reject(obj)
同Promise.resolve(obj)
类似。
Promise.all()
和Promise.race()
都是接受数组为参数,数组中的每个值都会被Promise.resolve
包装成promise
对象(《ECMAScript 6 入门》
中这么说,其实实现源码时不需要都转成promise
对象,内部做个promise
对象记数的变量,遇到普通值直接当做Resolved
,变量-1就好了,jq
中就是这样处理的),这两个方法都会返回一个promise
对象。
两个方法都是数组中任意项出现错误则立马触发reject(error)
,并锁定状态。其中all()
是数组中所有的项目都是Resolved
状态(内部计数变量减至0),会触发resolve(posts)
,posts
为含有n
个元素的数组,每个元素都是数组,包含对象项的状态变化的触发参数。race()
则是只要有一项,状态变为Resolved
,直接触发resolve(参数)
并锁定状态,参数为当前项的触发参数。
let p = Promise.race([p1,p2,p3]).then(sucFun).catch(errFun);
let t = Promise.all([t1,t2,t3]).then(sucFun).catch(errFun);
ES6
的异步编程的方式是Generator
+ promise
。promise
已经可以独立的完成异步编程了,为什么还要引入Generator
呢?
promise
编程通过then
的写法,把回调串联起来,链式调用很方便,是命令式的写法。通常更直观的编程方式是声明式的,人对视觉的认知是快过逻辑的,所见即所得,更符合直觉。如果能把异步代码按照同步方式顺序书写,将更直观。这必须要解决两个问题,一个是进程控制权的延迟转移问题,一个是关于错误抛出而调用栈中执行上下文已经无法追溯的问题。
Generator
是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
语句,定义不同的内部状态。执行Generator
函数会返回一个遍历器对象,可通过其依次遍历Generator
函数内部的每一个状态。
function* gen() {
yield 'hello';
yield 'world';
return 'ending';
}
let hw = gen();
hw.next(); // {value: 'hello', done: false}
hw.next(); // {value: 'world', done: false}
hw.next(); // {value: 'ending', done: true}
hw.next(); // {value: undefined, done: true}
Generator
是语言底层的实现,是一种“状态机”,yield
关键字,表示暂停。让我们从调用栈的角度来理解。
ECMAScript
是单线程语言,只能保持一个调用栈,属于传统的子例程,它的执行上下文是采用后进先出的形式,当前上下文执行到一个函数时或者新的代码块,会生成新的执行上下文,push
到调用栈的顶层,当该上下文完全执行完毕会被pop
,把执行权交回,继续执行父层的执行上下文。
Generator
是一种“协程”的实现,每个Generator
执行后都会有一个栈,独立于ECMAScript
的调用栈,不止一个栈的同时存在,会占用更多的内存。但是与普通的多线程不同,同一时间只会有一个栈是运行状态。而调用next()
的过程是转交执行权给Generator
对象的过程,遇到yield
会返回交还执行权给ES
的调用栈。因此yield
是真正意义的“暂停”,这也是被称为“状态机”的原因。正是由于这个特性,使得Generator
内部的try...catch
可以捕获next()
操作时,Generator
内产生的错误。
对Generator
执行后返回的Iterator
对象调用next()
的时候,执行权会转交给Generator
,执行到
第一个yield
处,并对其后的表达式求值并返回{value: 值,done: false}
(此时执行权会被交回),再次调用next(value)
,之前的yield
语句的值会变为value
,并且执行到下一个yield
语句,重复一样的过程。当调用next()
后遇不到yield
,则执行完后返回{value: undefined, done: true}
,且以后每次调用next()
均返回{value: undefined, done: true}
。若中途遇到return
关键字,则返回{value: 值, done: true}
,之后均返回{value: undefined, done: true}
。
除了next(value)
,还可以使用return(value)
throw(value)
。return(value)
等同于内部直接执行了return value;
时的效果,返回{value: value, done: true}
,throw(value)
则相当于,在内部上次返回值的yield
位置抛出错误,value
是错误信息。若抛出的错误,在Generator
内部没有顺利捕获,则退出Generator
后,执行权回归ES
调用throw(value)
的执行上下文位置,抛出错误,可被try...catch
语句顺利捕获。
let gen = function* gen(){
try {
yield console.log('hello');
} catch (e) {
console.log('内部捕获 inner');
}
yield console.log('world');
}
let g = gen();
try{
g.next();
g.throw();
g.throw();
} catch(e) {
console.log('外部捕获 outer');
}
// hello
// 内部捕获 inner
// 外部捕获 outer
Generator
还可以使用yield*
后跟Iterator
对象,则会在Iterator
对象每次next()
返回时yield
都返回一次。
let gen = function* () {
let v1 = yield 1;
let v2 = return 2;
};
let newGen = function* () {
let v1 = yield* gen();
let v2 = yield* [11, v1];
};
let iter = newGen();
iter.next(); // {value: 1, done: false}
iter.next(); // {value: 11, done: false}
iter.next(); // {value: 2, done: false}
iter.next(); // {value: undefined, done: true}
Generator
有出色的控制流管理,但仅仅如此还不够,因为必须要ES
调用栈的代码去控制Generator
的next
调用的时机。如果yield
后跟着promise
对象,则需要等该对象锁定状态后才能调用next
,可以配合then
完成这件事,递归实现自动的next
流程控制。
/* 回调函数 */
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
/* Promise */
Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
})
.catch(function (error) {
// Handle any error from step1 through step4
});
/* Generator + Promise */
let gen = function* (){
try {
var value1 = yield step1();
var value2 = yield step2(value1);
var value3 = yield step3(value2);
var value4 = yield step4(value3);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
};
spawn(gen);
// 自动流程管理
// 把yield返回的xx.value包装成promise对象,通过then绑定触发next()操作
// 借助promise本身的参数传递机制,通过next(value)使Generator函数中代码正确执行
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
step(function() { return gen.next(undefined); });
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
});
}
自动流程管理的模块,通常是不变的。因此简单的声明式Generator
写法就完成了头痛的异步编程,并且一目了然。spawn(genF)
会返回promise
对象,因此可在其后使用then
添加回调。
ES7
更近了一步,新增了Async
函数,直接执行就自动进行流程管理。本质上是Generator
的语法糖,等同于上面代码spawn(gen)
的表现,返回promise
对象。
let asy = async function (){
try {
var value1 = await step1();
var value2 = await step2(value1);
var value3 = await step3(value2);
var value4 = await step4(value3);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
};
asy();
到此,便是ES
目前异步编程的最终方案了。
看到很多人说,ES6
的很多语法像python
,也有人说借鉴了C#
,我都没学过,所以并不了解。原来下意识认为语言应该语法上差不太多,况且js
是门高级语言,现在看来自己还是太年轻了,ES6
的改动不可谓不大,功能强大、复杂了很多。通过对它的学习,对以后学习其他语言,应该会很有帮助。
这篇文章,写了不止一周,算上学习规范的时间,差不多一个月了,真是抓狂想呕血,学到吐了好吗!不过,还是感谢自己的坚持,13既然装了,就要自己画上一个完美的结局 ╭( ・ㅂ・)و ̑̑