ECMAScript 6规范总结(长文慎入)

姜奇
2023-12-01

闲话

学习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支持;新增数据结构SetMap及弱引用的WeakSetWeakMap,可去重存储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提案 ‘::’简化bindapplycall

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) => ({
    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


4、class

替代传统构造函数。不会变量提升。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


模块化Moudule

ES6 新增模块(module)体系,有效解决命名冲突、复杂依赖的历史问题。模块内默认严格模式’use strict’

commonjs规范一样可以支持循环依赖(严格说 commonjs 只能算一半支持,ES6规范完全支持),但却是更底层的,静态化的处理,使得编译时就能确定模块的依赖关系,以及输入和输出的变量(引入的变量为动态引用,会一直随着源模块变量的值的变化而变化)。

对commonjs(这里用A->B表示A模块引用B模块):A->B且B->A。当从A开始执行时,B第一次引用的A只是执行一部分的,A第一次引用的B是全部的,但建立在B获取到不完全的A的基础上。然后等A执行完,A、B模块才能被无bug的引用(详情见《es6入门》里module章节的解释)

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';  // 继承,直接引入并全部传出


类型升级


[Number]

1、处理数值的方法迁移到Number函数上,使语言结构更清晰。如全局方法isFinite()、isNaN()、parseInt()、parseFloat(),其中判断数值的Number.isFinite()、Number.isNaN()若参数不为数值、布尔值,直接返回 false

2、在全语言的支持上,认为 +0 -0 不同,NaN 与自身相同(ES6向后兼容,但在新增API中判断值的相等性时支持)

3个老问题:
1、二进制、八进制的表示存在歧义
2、由于以浮点数存储,因意外的误差导致不相等
3、不在 -2^532^53之间的数无法正确表示、运算

1、优化了二进制、八进制的表示,分别为0b/0B0o/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()可对数值截取整数


[String]

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模式

[RegExp]

正则的作用是快速匹配字符串

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]

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对象(如Array实例的[Symbol.iterator]函数)的_proto_原型都会是一个有[Symbol.iterator]接口的对象,执行anIteratorSymbol.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]

老问题:

  • 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


[Object]

对象的简洁表示,已经在”语法升级”中做了说明。

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)
);


[Set/Map]

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()) {}

WeakSetWeakMap是ES6提供的两种弱引用类型。分别只支持 value为对象/key为对象 的情形。与SetMap不同,储存的数据如果在环境中不再被引用,则会被垃圾清理机制,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上是抛出错误

ProxyReflect的支持操作是一一对应的。使用方式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


异步编程

Promise

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);


Generator

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添加回调。


Async

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既然装了,就要自己画上一个完美的结局 ╭( ・ㅂ・)و ̑̑

 类似资料: