ES6 核心特性解析

May 31, 2019

ECMAScript

前言

我最近把原先的 .TOP 域名更换到 .CN 域名,并且用 Gatsby 重建个人博客,由于之前是采用 HTTPS 部署的方式绕过阿里云的域名备案系统,所以不必备案。

更换 CN 域名后,这招不管用了,😭😭 域名必须要备案了,等待幕布邮寄中……

此文用来记录 ES6 的核心特性,以及一些容易被遗落的知识点。

基础篇

Let + Const

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

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

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

function f(x, y = 12) {
  // 当形参 y 没有被传递时,或传递 undefined, 那么 y === 12
  return x + y;
}

f(3) === 15; // true
function f(x, ...y) {
  // y 被视为一个数组
  return x * y.length;
}

f(3, "hello", true) === 6; // true
function f(x, y, z) {
  return x + y + z;
}

// 将数组装换为参数序列
f(...[1, 2, 3]) === 6; // true

解构(Destructuring)

解构赋值只在被解构字段的值和 undefined 严格相等时生效

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

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

var { op, lhs, rhs } = getASTNode();
// 等同于下面的写法
var { op: op, lhs: lhs, rhs: rhs } = getASTNode();

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

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

// 参数解构并赋值
var [a = 1] = [];
a === 1; // true

箭头函数 & 语法层面上的 This(Arrows and Lexical This)

// 除了支持返回语句,还可以将表达式作为返回主体

const foo = () => ({ name: "es6" });
const bar = num => (num++, num ** 2);

foo(); // 返回一个对象 { name: 'es6' }
bar(3) === 16; // true,这里使用了逗号操作符,会返回最后一个表达式的值

JS 中 this 的指向一直让开发者捉摸不透,初学者很容易误用。

其实,可以总结为一句话: this 永远指向调用它的那个对象

而箭头函数则改写了这一规则,就是 箭头函数共享当前代码上下文的 this

可以理解为以下的阐述:

  • 箭头函数不会创建自己的 this它只会在声明了箭头函数的作用域的上一层作用域去继承 this,如果上一层依旧是箭头函数,则继续向上查找,直至全局作用域,此时 this 指向 global 对象。

这里的作用域存在于函数定义时

因此,在下面的代码中,传递给 setTimeout 的函数内的 this 与 sayHello 函数中的 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
// 作为对象的方法调用,this 指向 bob

hello();
// hello, I am undefined
// 作为普通函数调用,即 window.hello(),this 指向 global

hello.call({ name: "Mike" });
// hello, I am Mike
// call,apply 调用,显式定义 this 对象

做个小测试,以下代码的执行结果是什么?

language = "Python";
const obj = {
  language: "Java",
  speak() {
    language = "Go";
    return function() {
      return () => {
        console.log(`I'm learning ${this.language}`);
      };
    };
  },
};

obj.speak()()();

箭头函数还具有以下特点:

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

Symbols

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值,类似于 UUID。

// 每个 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]);
  },
};

typeof key; // symbol

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

Symbol 的一些特性必须依赖于浏览器的原生实现,不可被 transpiled 或 polyfilled。

应用场景

  • 设计优雅的数据对象,让“对内操作”和“对外选择性输出”变得简便。

在实际应用中,我们经常会需要使用 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']

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

因此,当使用 JSON.stringify() 将对象转换成 JSON 字符串的时候,Symbol 类型的 key 会被忽略:

JSON.stringify(obj); // {"age":18,"title":"Engineer"}
  • 消除魔术字符串
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),可以是对象、函数以及另一个 Map

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 的作用有三个:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • 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 是可遍历的,因为具有 Symbol.iterator 属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 value 和 done 两个属性。

JS 内置了许多具备 Iterator 接口的数据结构:

  • Array
  • Map、WeakMap
  • Set、WeakSet
  • 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;
> true

这是因为 JS 的数值规范采用了 IEEE 754 标准,所以数字都是以 64 位双精度浮点数据类型储存。

即 JS 语言根本没有整数,所有数字都是小数!当我们以为在用整数进行计算时,都会被转换为小数。

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

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

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

新增的 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

显然,5.551115123125783e-17 这个误差已经没有存在的意义。

借助 Number.EPSILON 我们可以达到 0.1 + 0.2 === 0.3 的效果。

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 的尾调用优化只在严格模式下开启。

代理(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"
// 代理会将所有应用到它的操作转发到 target 上
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

Proxy 不能被 transpiled or polyfilled,亲自入的坑,由于在项目中使用了 Mobx5.x,其内部是用 Proxy 写的,结果 IE11 不支持 Proxy,只得回退版本 Mobx 到 4.x。

反射(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;
O.__proto__.d = 4

Reflect.getOwnPropertyDescriptor(O, "b");
// { value: 2, writable: false, enumerable: false, configurable: false }

Reflect.ownKeys(O);
// ['a', 'b', Symbol(c)]

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 属性        

Object.keys(O)  
//  ['a'] 自身除 Symbol 外可枚举属性       

Reflect.ownKeys(O)  
//  [ 'a', 'b', Symbol(c) ] 自身所有属性   

for...in  
//  a d 自身和原型链,除 Symbol 外的可枚举属性 

生成器(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();
// 调用 next,函数开始执行,直到遇见 yield 暂停,返回 yield 后面的值
// {value: undefined, done: false}

fullName.next("Handsome");
// 调用 next,恢复函数执行,并传参 Handsome,yield 被 Handsome 替代,因此 firstName 的值变为 Handsome
// 遇到 yield 再次暂停执行
// {value: undefined, done: false}

fullName.next("Jack");
// 调用 next,恢复函数执行,传参 Jack,yield 被 Jack 替代,因此 secondName 的值变为 Jack
// Handsome Jack
// {value: undefined, done: true}

使用 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)

总结

作为 ECMAScript 近几年最为重要的版本,学习它,能帮助你在实际项目中 以优雅的手段解决问题,简化代码逻辑,提高运行效率

附上一张我之前精心整理的思维导图,由于内容较多,可以选择在新标签页中打开,放大观看。

本文参考资料:


Written by B2D1(包邦东)