All Articles

什么?ES6 中还有 Tail Calls!

前言

先吐槽一件事,最近把原先的 TOP 域名更换到 CN 域名,并且用 Gatsby 重建个人站点,之前是用采用 HTTPS 部署的方式绕过阿里云的域名备案系统。更换 CN 域名后,这招不管用了,😭😭 域名必须要备案了,等待幕布邮寄中……

有人要问了,都 9102 年,ES10 都出来了,怎么还在讲 ES6,非也!本文针对 ES6 几个不为人知、和重要的特性做讲解,精彩的在后面!

基础篇

Let + Const

ES6 除了固有的函数作用域,还引入了块级作用域({})

function f() {
  {
    let x; // ①
    {
      // 包含在当前块中,与 ① 中的 x 分属不同作用域
      const x = "sneaky";
      // 错误,const 定义的变量不可以重新赋值,如果 const 定义了一个对象,那么对象的属性是可以修改的
      x = "foo";
    }
    // let 定义的变量可以重新赋值
    x = "bar";
    // 错误,x 在 ① 块中已被定义
    let x = "inner";
  }
}

默认、剩余、展开参数(Default + Rest + Spread)

function f(x, y=12) {
  // y 等于 12 如果不传递 (或者传递 undefined)
  return x + y;
}
f(3); // 15
function f(x, ...y) {
  // y 是一个数组
  return x * y.length;
}
f(3, "hello", true); // 6
function f(x, y, z) {
  return x + y + z;
}
// 将数组的每一项作为参数传递
f(...[1,2,3]); // 6

解构(Destructuring)

var [a, ,b] = [1,2,3];
a === 1; // true
b === 3; // true

var { op: a, lhs: { op: b }, rhs: c } = getASTNode()

// var {op: op, lhs: lhs, rhs: rhs} = getASTNode()
var {op, lhs, rhs} = getASTNode() 

// 参数解构
function g({name: x}) {
  console.log(x);
}
g({name: 5})

var [a] = [];
a === undefined; // true

var [a = 1] = [];
a === 1; // true

// 解构 + 默认参数
function r({x, y, w = 10, h = 10}) {
  return x + y + w + h;
}
r({x:1, y:2}) === 23 // true

箭头函数(Arrows and Lexical This)

// 除了支持返回语句,还可以将表达式作为返回主体
const foo = () => ({ name: 'es6' }); 
const bar = (num) => (num++, num ** 2);

foo();  // 返回一个对象 { name: 'es6' }
bar(3); // 执行多个表达式,并返回最后一个表达式的值 16

JS 中 this 的指向问题一直都是面试高频考点,不少人在实战中也掉入坑中,总结起来就是一句话:“ this 永远指向调用它的那个对象”,而箭头函数则改写了这一规则,就是

箭头函数共享当前代码上下文的 this

什么意思呢?可以理解为

  • 箭头函数不会创建自己的 this它只会从自己的作用域链的上一层继承 this,如果上一层还是箭头函数,则继续向上查找,直至全局作用域,在浏览器环境下即 window
  • 函数具有作用域链,对象则不具有

因此,在下面的代码中,传递给 setInterval 的函数内的 thissayHello 函数中的 this 一致:

const bob = {
  name: 'Bob',
  sayHello() {
    setTimeout(() => {
      console.log(`hello, I am ${this.name}`);
    }, 1000);
  }
};
const hello = bob.sayHello;

bob.sayHello();
// hello, I am Bob 
// 作为对象的方法调用,sayHello的this指向bob 
hello();
// hello, I am undefined 
// 作为普通函数调用,相当于window.hello(),this指向全局对象
hello.call({name:'Mike'});
// hello, I am Mike 
// call,apply调用,第一个参数为this指向的对象
language = 'Python';
const obj = {
  language: 'TS',
  speak() {
    language = 'GO';
    return function() {
      return () => {
        console.log(`I speak ${this.language}`);
      };
    };
  }
};

obj.speak()()(); // 做个小测试,会打印什么呢?

箭头函数还有以下特点

  • 由于箭头函数没有自己的 this 指针,通过 callapply 调用,第一个参数会被忽略
  • 不绑定 Arguments 对象,其引用上一层作用域链的 Arguments 对象
  • 不能用作构造器,和 new 一起用会抛出错误。
  • 没有 prototype 属性。

现在你应该明白为何 React 中的函数写法都为箭头函数,就是为了绑定 this

Symbols

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值,它的功能类似于一种标识唯一性的 ID

// 每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false
const s1 = Symbol('macOS');
const s2 = Symbol('macOS');

// Symbol.for 机制有点类似于单例模式
const s3 = Symbol.for('windows'); // 注册一个全局 Symbol
const s4 = Symbol.for('windows'); // 已存在相同名称的 Symbol,返回全局 Symbol

s1 === s2; // false
s3 === s4; // true
let key = Symbol('key');

function MyClass(privateData) {
  // 注意,Symbol值作为对象属性名时,不能用点运算符
  this[key] = privateData;
}

MyClass.prototype = {
  doStuff() {
    console.log(this[key]);
  }
};

// Symbol的一些特性必须要浏览器的原生实现,不可被 transpiled 或 polyfilled
typeof key // symbol

let c = new MyClass('hello');
c.key; // undefined
c[key]; // hello

应用场景

  • 更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。

在实际应用中,我们经常会需要使用 Object.keys() 或者 for...in 来枚举对象的属性名,那在这方面,Symbol 类型的 key 表现的会有什么不同之处呢?来看以下示例代码:

let obj = {
   [Symbol('name')]: '一斤代码',
   age: 18,
   title: 'Engineer'
}

Object.keys(obj)   // ['age', 'title']

for (let p in obj) {
   console.log(p)   // 分别会输出:'age' 和 'title'
}

Object.getOwnPropertyNames(obj)   // ['age', 'title']

也正因为这样一个特性,当使用 JSON.stringify() 将对象转换成 JSON 字符串的时候,Symbol 属性也会被排除在输出内容之外:

JSON.stringify(obj)  // {"age":18,"title":"Engineer"}

由上代码可知,Symbol 类型的 key 是不能通过 Object.keys() 或者 for...in 来枚举的,所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol 来定义。

  • 消除魔术字符串
const TYPE_AUDIO = 'AUDIO'
const TYPE_VIDEO = 'VIDEO'
const TYPE_IMAGE = 'IMAGE'

function handleFileResource(resource) {
  switch(resource.type) {
    case TYPE_AUDIO:
      playAudio(resource)
      break
    case TYPE_VIDEO:
      playVideo(resource)
      break
    case TYPE_IMAGE:
      previewImage(resource)
      break
    default:
      throw new Error('Unknown type of resource')
  }
}

上面的代码中那样,我们需要为常量赋一个唯一的值(比如这里的 'AUDIO'),'AUDIO' 就是一个魔术字符串,它本身没意义,只是为了保证常量唯一的关系。常量一多,就变得十分臃肿且难以理解

现在有了 Symbol,我们大可不必这么麻烦了:

// 保证了三个常量的值是唯一的
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()

增强的对象字面量(Enhanced Object Literals)

const obj = {
  // 允许设置原型
  __proto__: theProtoObj,
  // 允许覆盖属性
  ['__proto__']: somethingElse,
  // 属性简写,等于 ‘handler: handler’
  handler,
  // 计算 (动态) 属性名
  ['prop_' + (() => 42)()]: 42
};
obj.prop_42 // 42
obj.__proto__ // somethingElse

__proto__ 需要原生支持,它在之前的 ECMAScript 版本中被移除,但大多数浏览器都实现了这一特性,包括 Node 环境

Map + Set + WeakMap + WeakSet

Set

Set 是 ES6 中新增的数据结构,它允许创建唯一值的集合。集合中的值可以是简单的基本类型(如字符串或数值),但更复杂的对象类型(如对象或数组)也可以,亦或是一个新的 Set

let animals = new Set();

animals.add('🐷');
animals.add('🐼');
animals.add('🐢');
animals.add('🐿');
console.log(animals.size); // 4
animals.add('🐼');
console.log(animals.size); // 4

console.log(animals.has('🐷')); // true
animals.delete('🐷');
console.log(animals.has('🐷')); // false

animals.forEach(animal => {
  console.log(`Hey ${animal}!`);
});

// Hey 🐼!
// Hey 🐢!
// Hey 🐿!

animals.clear();
console.log(animals.size); // 0

我们还可以传入一个数组来初始化集合

let myAnimals = new Set(['🐷', '🐢', '🐷', '🐷']);

myAnimals.add(['🐨', '🐑']);
myAnimals.add({ name: 'Rud', type: '🐢' });
console.log(myAnimals.size); // 4

// Set 内置了遍历器,可以调用 forEach, for…of
myAnimals.forEach(animal => {
  console.log(animal);
});

// 🐷
// 🐢
// ["🐨", "🐑"]
// Object { name: "Rud", type: "🐢" }

Map

与普通对象(Object)不同,Map 的键名(Key)可以是任何类型,不再局限于字符串(String),包括但不限于 objectsfunctions

let things = new Map();

const myFunc = () => '🍕';

things.set('🚗', 'Car');
things.set('🏠', 'House');
things.set('✈️', 'Airplane');
things.set(myFunc, '😄 Key is a function!');

things.size; // 4

things.has('🚗'); // true

things.has(myFunc) // true
things.has(() => '🍕'); // false
things.get(myFunc); // '😄 Key is a function!'

things.delete('✈️');
things.has('✈️'); // false

things.clear();
things.size; // 0

// 链式设置键值对
things.set('🔧', 'Wrench')
      .set('🎸', 'Guitar')
      .set('🕹', 'Joystick');

const myMap = new Map();

// 甚至键名可以是另一个 Map
things.set(myMap, 'Oh gosh!');
things.size; // 4
things.get(myMap); // 'Oh gosh!'

可以通过传入包含两个元素的数组来初始化 Map

const funArray = [
  ['🍾', 'Champagne'],
  ['🍭', 'Lollipop'],
  ['🎊', 'Confetti'],
];

let funMap = new Map(funArray);
funMap.get('🍾'); // Champagne

WeakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。它最重要的特性是 WeakMap 保持了对键名所引用的对象的弱引用

我们可以通过 Node 来证明一下这个问题:

// 允许手动执行垃圾回收机制
node --expose-gc

global.gc();
// 返回 Nodejs 的内存占用情况,单位是 bytes
process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4M

let map = new Map();
let key = new Array(5 * 1024 * 1024); // new Array 当为 Obj
map.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46751472 注意这里大约是 44.6M

// 所以当你设置 key = null 时,只是去掉了 key 对 Obj 的强引用
// 并没有去除 arr 对 Obj 的强引用,所以 Obj 还是不会被回收掉
key = null;
global.gc();
process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M

// 这句话其实是无用的,因为 key 已经是 null 了
map.delete(key);
global.gc();
process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M
node --expose-gc

global.gc();
process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4M

const wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M

// 当我们设置 key = null 的时候,就只有 wm 对所引用对象的弱引用
// 下次垃圾回收机制执行的时候,该引用对象就会被回收掉。
key = null;
global.gc();
process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M

应用场景

传统使用 jQuery 的时候,我们会通过 $.data() 方法在 DOM 对象上储存相关信息(就比如在删除按钮元素上储存帖子的 ID 信息),jQuery 内部会使用一个对象管理 DOM 和对应的数据,当你将 DOM 元素删除,DOM 对象置为空的时候,相关联的数据并不会被删除,你必须手动执行 $.removeData() 方法才能删除掉相关联的数据,WeakMap 就可以简化这一操作:

let wm = new WeakMap(), element = document.querySelector(".element");
wm.set(element, "data");

let value = wm.get(elemet);
console.log(value); // data

element.parentNode.removeChild(element);
element = null;

WeakSet

特性与 WeakMap 相似

遍历器(Iterators + For..Of)

遍历器(Iterator)它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 Iterator 的作用有三个:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”(iterable

let fibonacci = {
  [Symbol.iterator]() {
    let pre = 0, cur = 1;
    return {
      next() {
        [pre, cur] = [cur, pre + cur]; // 数组解构
        return { done: false, value: cur }
      }
    }
  }
}

for (var n of fibonacci) {
  // 当n超过1000时停止
  if (n > 1000)
    break;
  console.log(n);
}

上面代码中,对象 fibonacci 是可遍历的(iterable),因为具有 Symbol.iterator 属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 valuedone 两个属性

原生具备 Iterator 接口的数据结构如下

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

for…in 和 for..of 的差别

  • for…in 遍历键名(Key)并转化为字符串,for…of 遍历键值(Value)
  • for…in 语句以任意顺序遍历一个对象自有的、继承的、可枚举的、非 Symbol 的属性
  • for…in 更适合遍历对象,for…of 更适合遍历数组。
for (let i in [1, 2, 3]) {
  console.log(typeof i); // string 数组下标被转化字符串
  console.log(i); // '1', '2', '3'
}

var triangle = { a: 1, b: 2, c: 3 };

function ColoredTriangle() {
  this.color = 'red';
}

ColoredTriangle.prototype = triangle;

var obj = new ColoredTriangle();

for (var prop in obj) {
  if (obj.hasOwnProperty(prop)) { // 如果去了 hasOwnProperty() 这个约束条件会怎么样?
    console.log(`obj.${prop} = ${obj[prop]}`); // obj.color = red
  }
}

新增 API(Math + Number + String + Object APIs)

我们先来看看新增的 Number.EPSILON,不少人都是懵逼的状态,WTF?
先来看看的 JS 世界中的一道送命题

0.1 + 0.2 // 结果0.30000000000000004 而不是0.3

事出必有因,这是因为 JS 的数值采用了 IEEE 754 标准,而且 JS 是弱类型语言,所以数字都是以64位双精度浮点数据类型储存。也就是说,JS 语言底层根本没有整数,所有数字都是小数!当我们以为在用整数进行计算时,都会被转换为小数

而浮点数都是以多位二进制的方式进行存储的

十进制的0.1用二进制表示为:0.0 0011 0011 0011 0011…,循环部分是0011
十进制0.2用二进制表示为:0.0011 0011 0011 0011…,循环部分是0011

由于存储空间有限,最后计算机会舍弃后面的数值,所以我们最后就只能得到一个近似值

JS中采用的 IEEE 754 的双精度标准也是一样的道理在存储空间有限的情况下,当出现这种无法整除的小数的时候就会取一个近似值,在 JS 中如果这个近似值足够近似,那么 JS 就会认为他就是那个值。

console.log(0.1000000000000001) 
// 0.1000000000000001 (中间14个0,不会被近似处理,输出本身)
console.log(0.10000000000000001) 
// 0.1 (中间15个0,js会认为两个值足够近似,所以输出0.1)

那么这个近似的界限如何判断呢?

ES6的 Number.EPSILON就是一个界限,它表示 1 与大于 1 的最小浮点数之间的差。

对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方

Number.EPSILON === Math.pow(2, -52)
// true
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"

Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

0.1 + 0.2 - 0.3
// 5.551115123125783e-17

5.551115123125783e-17.toFixed(20)
// '0.00000000000000005551'
0.00000000000000005551 < 0.00000000000000022204 // true

显然,0.30000000000000004 不存在误差,不会被近似处理

我们可以通过以下手段来达到我们想要的效果

function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}

0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true

其他一些新增的 API

Number.isInteger(Infinity) // false
Number.isNaN("NaN") // false

Math.sign(-5) // 判断一个数到底是正数、负数、还是零 -1
Math.hypot(3, 4) // 返回所有参数的平方和的平方根 5
Math.imul(-2, -2) // 返回两个数以 32 位带符号整数形式相乘的结果 4

"abcde".includes("cd") // true
"abc".repeat(3) // "abcabcabc"

Array.from(document.querySelectorAll("*")) // 返回一个真正的数组
Array.of(1, 2, 3) // [1,2,3]
[0, 0, 0].fill(7, 1) // [0,7,7]
[1,2,3].findIndex(x => x == 2) // 1
["a", "b", "c"].entries() // [0, "a"], [1,"b"], [2,"c"]
["a", "b", "c"].keys() // 0, 1, 2
["a", "b", "c"].values() // "a", "b", "c"

Object.assign(Point, { origin: new Point(0,0) }) // 合并对象

二进制和八进制字面量(Binary and Octal Literals)

0b111 === 7 // true  二进制
0o111 === 73 // true  八进制
0x111 === 273 // true  十六进制

进阶篇

尾递归(Tail Calls)

假设现在要实现一个阶乘函数,即 5!= 120,我们很容易想到递归实现

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

但递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生”栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生”栈溢出”错误。

何为调用记录,在示例代码中,由于最后一步返回了一个表达式,内存会保留 n 这个变量的信息和 factorial(n - 1) 调用下一次函数的位置,形成一层层的调用栈

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身,返回函数本身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。尾递归优化如下

function factorial(n, acc = 1) {
    "use strict";
    if (n <= 1) return acc;
    return factorial(n - 1, n * acc);
}

factorial(100000)

由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 也是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署”尾调用优化”。这就是说,在 ES6 中,只要使用尾递归,就不会发生栈溢出,相对节省内存。

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。

反射(Reflect)

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect 对象的设计目的有这样几个。

  • 将Object对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上
  • 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false
var O = {a: 1};
Reflect.defineProperty(O, 'b', {value: 2});
O[Symbol('c')] = 3;

Reflect.ownKeys(O); // ['a', 'b', Symbol(c)]
Reflect.getOwnPropertyDescriptor(O, 'b'); 
// { value: 2, writable: false, enumerable: false, configurable: false }
function C(a, b){
  this.c = a + b;
}
var instance = Reflect.construct(C, [20, 22]);
instance.c; // 42

获取属性名的方法有很多,以上面的代码为例子,它们的区别如下

方法 结果 解释
Object.getOwnPropertyNames(O) [ 'a', 'b' ] 获取除 Symbol 外的所有属性
Object.getOwnPropertySymbols(O) [ Symbol(c) ] 只获取 Symbol 属性
OReflect.ownKeys(O) [ 'a', 'b', Symbol(c) ] 获取所有属性
for...in a 获取除 Symbol 外的可枚举属性

代理(Proxy)

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

// 代理一个对象
var target = {};
var handler = {
  get: function (receiver, name) {
    return `Hello, ${name}!`;
  }
};

var p = new Proxy(target, handler);
p.world; // "Hello, world!"
// 代理一个函数
var target = function () { return "I am the target"; };
var handler = {
  apply: function (receiver, ...args) {
    return "I am the proxy";
  }
};

var p = new Proxy(target, handler);
p(); // "I am the proxy"
// 代理会将所有应用到它的操作转发到这个对象上
let target = {};
let p = new Proxy(target, {});

p.a = 37; 
target.a; // 37 操作转发到目标
// 如何实现 a == 1 && a == 2 && a == 3,利用Proxy的get劫持
const a = new Proxy(
  {},
  {
    val: 1,
    get() {
      return () => this.val++;
    }
  }
);
a == 1 && a == 2 && a == 3; // true
由于 ES5 的限制,Proxy 不能被 transpiled or polyfilled,自己亲自入的坑,由于在项目中使用了 Mobx5.x,其内部是用 Proxy 写的,结果 IE11 不支持 ES6,只得回退版本 Mobx 到 4.x

生成器(Generators)

Generator 函数是 ES6 提供的一种异步编程解决方案。 Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态

function* helloWorldGenerator() {
  yield 'hello'; // yield使Generator函数暂停了执行,并将结果返回给调用者
  yield 'world'; // 当下一次调用时,从它中断的地方恢复执行
  return 'ending';
}

var hw = helloWorldGenerator();
a = hw.next(); // { value: 'hello', done: false }
b = hw.next(); // { value: 'world', done: false }
c = hw.next(); // { value: 'ending', done: true }

可以利用这种暂停执行的特性,来实现惰性求值

向Generator传递数据

function* sayFullName() {
  const firstName = yield;
  const secondName = yield;
  console.log(firstName + ' ' + secondName);
}
let fullName = sayFullName();
fullName.next(); 
// 第一次调用,代码暂停在 const firstName = yield,因为没有通过 yield 发送任何值,因此 next 将返回 undefined
fullName.next('Handsome');
// 第二次调用,传入了值 Handsome,yield 被 Handsome 替代,因此 firstName 的值变为 Handsome,代码执行恢复
// 直到再次遇到 const secondName = yield 暂停执行
fullName.next('Jack');
// 第三次调用,传入了值 Jack,yield 被 Jack 替代,因此 secondName 的值变为 Jack,代码执行恢复
// 打印 Handsome Jack

使用Generator处理异步调用

let generator;
let getDataOne = () => {
  setTimeout(() => {
    generator.next('dummy data one');
  }, 1000);
};
let getDataTwo = () => {
  setTimeout(() => {
    generator.next('dummy data one');
  }, 1000);
};

function* main() {
  let dataOne = yield getDataOne();
  let dataTwo = yield getDataTwo();
  console.log(dataOne, dataTwo);
}
generator = main();
generator.next();
// 执行 getDataOne(),然后 yield 暂停
// 直至一秒后 generator.next('dummy data one') 恢复代码执行,并赋值 dataOne
console.log('i am previous print');
// i am previous print
// dummy data one dummy data one

Promises

Promises 是一个异步编程的解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

function timeout(duration = 0) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, duration);
    })
}

var p = timeout(1000).then(() => {
    return timeout(2000);
}).then(() => {
    throw new Error("hmm");
}).catch(err => {
    return Promise.all([timeout(100), timeout(200)]);
})

这里强调几点

  • 不要剥夺函数 return 的能力,很多人写 Promise,照样有大量嵌套,掉进 Promise 地狱,要记得及时 return,避免嵌套
  • 当需要多个请求全部结束时,才更新数据,可以用 Promise.all(fetch1,fetch2)
  • 当需要从多个请求中,接受最先返回数据的那个请求,可以用 Promise.race(fetch1,fetch2)

结尾

ES6 是 ECMAScript 一个非常重要的版本,我们必须深入理解,不仅能提高我们书写代码的能力,还能增强业务能力

附上一张我之前精心整理的思维导图

本文参考资料