前端进阶-ES6内置功能

Symbol

Symbol 简介

Symbol 是 JS 原始数据类型列表中(numbersstringsbooleansnullundefined)的最新补充。Symbol 是一个唯一标识符,常用于唯一标识对象中的属性。

碗这个对象中有几个水果属性(水果也是对象),当有两个相同的水果在碗里时,会出现混乱,我们不知道什么时候拿哪一个,这就是问题。我们需要一种方式来唯一的标识这这些香蕉。

Symbol 标识符

Symbol 是一种独特的且不可变的数据类型,经常用来标识对象属性

const sym1 = Symbol('apple');
console.log(sym1); // Symbol(apple)

它将创建唯一的标识符,并将其存储在 sym1 中。描述 “apple” 只是用来描述标识符的一种方式,但是不能用来访问标识符本身

const sym2 = Symbol('banana');
const sym3 = Symbol('banana');
console.log(sym2 === sym3); // false

描述只是用来描述符号,它并不是标识符本身的一部分。无论描述是什么,每次都创建新的标识符

示例说明,下面是代表上图中的 bowl(碗)的代码

const bowl = {
  'apple': { color: 'red', weight: 136.078 },
  'banana': { color: 'yellow', weight: 183.15 },
  'orange': { color: 'orange', weight: 170.097 }
};

碗中包含水果,它们是 bowl 的属性对象。但是,当我们添加第二个香蕉时,遇到了问题。

const bowl = {
  'apple': { color: 'red', weight: 136.078 },
  'banana': { color: 'yellow', weight: 183.151 },
  'orange': { color: 'orange', weight: 170.097 },
  'banana': { color: 'yellow', weight: 176.845 }
};
console.log(bowl);
// Object {apple: Object, banana: Object, orange: Object}

新添加的香蕉将上一个香蕉覆盖了。为了解决该问题,我们可以使用标识符。

const bowl = {
  [Symbol('apple')]: { color: 'red', weight: 136.078 },
  [Symbol('banana')]: { color: 'yellow', weight: 183.15 },
  [Symbol('orange')]: { color: 'orange', weight: 170.097 },
  [Symbol('banana')]: { color: 'yellow', weight: 176.845 }
};
console.log(bowl);
// Object {Symbol(apple): Object, Symbol(banana): Object, Symbol(orange): Object, Symbol(banana): Object}

通过更改 bowl 的属性并使用标识符,每个属性都是唯一的标识符,第一个香蕉不会被第二个香蕉覆盖。

迭代器协议和可迭代协议

ES6 中的两个新协议:

  • 可迭代协议
  • 迭代器协议

这两个协议不是内置的,但是它们可以帮助你理解 ES6 中的新迭代概念,就像给你展示标识符的使用案例一样。

可迭代协议

可迭代协议用来定义和自定义对象的迭代行为。也就是说在 ES6 中,你可以灵活地指定循环访问对象中的值的方式。对于某些对象,它们已经内置了这一行为。例如,字符串数组就是内置可迭代类型的例子。

const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const digit of digits) {
  console.log(digit);
}

任何可迭代的对象都可以使用新的 for...of 循环。

原理

为了使对象可迭代,它必须实现可迭代接口。接口其实就是为了让对象可迭代,它必须包含默认的迭代器方法。该方法将定义对象如何被迭代。

迭代器协议

迭代器协议用来定义对象生成一系列值的标准方式。实际上就是现在有了定义对象如何迭代的流程。通过执行 .next() 方法来完成这一流程

原理

当对象执行 .next() 方法时,就变成了迭代器.next() 方法是无参数函数,返回具有两个属性的对象

  • value:表示对象内值序列的下个值的数据
  • done:表示迭代器是否已循环访问完值序列的布尔值。如果 donetrue,则迭代器已到达值序列的末尾处;如果 donefalse,则迭代器能够生成值序列中的另一个值。
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const arrayIterator = digits[Symbol.iterator]();

console.log(arrayIterator.next());
console.log(arrayIterator.next());
console.log(arrayIterator.next());

/**
Object {value: 0, done: false}
Object {value: 1, done: false}
Object {value: 2, done: false}
*/

Set

数学意义上的集合(Set)

Set 就是唯一项的集合。例如,{2, 4, 5, 6} 是 Set,因为每个数字都是唯一的,只出现一次。但是,{1, 1, 2, 4} 不是 Set,因为它包含重复的项目(1 出现了两次!)。

Set(集合)

在 ES6 中,有一个新的内置对象的行为和数学意义上的集合相同,使用起来类似于数组。这个新对象就叫做“Set”。

Set 与数组之间的最大区别是:

  • Set 不基于索引,不能根据集合中的条目在集合中的位置引用这些条目
  • Set 中的条目不能单独被访问

基本上,Set 是让你可以存储唯一条目的对象。你可以向 Set 中添加条目删除条目,并循环访问 Set。这些条目可以是原始值对象

如何创建 Set

const games1 = new Set();
console.log(games1); // Set {},其中没有条目
// 根据值列表创建 Set,则使用数组
const games2 = new Set(['Super Mario Bros.', 'Banjo-Kazooie', 'Mario Kart', 'Super Mario Bros.']);
console.log(games2);
// Set {'Super Mario Bros.', 'Banjo-Kazooie', 'Mario Kart'}
// 会自动移除重复的条目 "Super Mario Bros.",很整洁!

修改 Set

const games = new Set(['Super Mario Bros.', 'Banjo-Kazooie', 'Mario Kart', 'Super Mario Bros.']);

games.add('Banjo-Tooie');
games.add('Age of Empires');
games.delete('Super Mario Bros.');

console.log(games);
// Set {'Banjo-Kazooie', 'Mario Kart', 'Banjo-Tooie', 'Age of Empires'}
games.clear()
console.log(games); // Set {}

.add() 添加不管成功与否,都会返回该 Set 对象。另一方面,.delete() 则会返回一个布尔值,该值取决于是否成功删除(即如果该元素存在,返回 true,否则返回 false)。

使用 Set

.size 属性可以返回 Set 中的条目数

const months = new Set(['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);
console.log(months.size); // 12

.has() 方法可以检查 Set 中是否存在某个条目。如果 Set 中有该条目,则 .has() 将返回 true。如果 Set 中不存在该条目,则 .has() 将返回 false

console.log(months.has('September')); // true

.values() 方法可以返回 Set 中的值.values() 方法的返回值是 SetIterator 对象。

console.log(months.values());
/**
SetIterator {'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'}
*/

Set 与迭代器

ES6 中的新可迭代协议和迭代器协议中,Set 是内置可迭代类型。这意味着循环时的两件事:

  • 可以使用 Set 的默认迭代器循环访问 Set 中的每一项。
  • 可以使用新的 for...of 循环来循环访问 Set 中的每一项。

使用 SetIterator

因为 .values() 方法返回新的迭代器对象(称为 SetIterator),你可以将该迭代器对象存储在变量中,并使用 .next() 访问 Set 中的每一项。

const iterator = months.values();
iterator.next();
// Object {value: 'January', done: false}
iterator.next();
// Object {value: 'February', done: false}
// ...,一直运行到 done 等于 true 时,标志着 Set 的结束。

使用for...of循环

const colors = new Set(['red', 'orange', 'yellow', 'green', 'blue', 'violet', 'brown', 'black']);
for (const color of colors) {
  console.log(color);
}
/**
red 
orange 
yellow 
green 
blue 
violet 
brown 
black
*/

WeakSet(弱集合)

WeakSet 和普通 Set 很像,但是具有以下关键区别:

  • WeakSet 只能包含对象
  • WeakSet 无法迭代,意味着不能循环访问其中的对象
  • WeakSet 没有 .clear() 方法
const student1 = { name: 'James', age: 26, gender: 'male' };
const student2 = { name: 'Julia', age: 27, gender: 'female' };
const student3 = { name: 'Richard', age: 31, gender: 'male' };

const roster = new WeakSet([student1, student2, student3]);
console.log(roster);
/**
WeakSet {Object {name: 'Julia', age: 27, gender: 'female'}, Object {name: 'Richard', age: 31, gender: 'male'}, Object {name: 'James', age: 26, gender: 'male'}}
*/
// 添加对象以外的内容,系统将报错!
roster.add('Amanda');
// Uncaught TypeError: Invalid value used in weak set(…)

垃圾回收

在 JavaScript 中,创建新的值时会分配内存,并且当这些值不再需要时,将自动释放内存。这种内存不再需要后释放内存的过程称为垃圾回收

WeakSet 通过专门使用对象作为键值来利用这一点。如果将对象设为 null,则本质上是删除该对象。当 JavaScript 的垃圾回收器运行时,该对象之前占用的内存将被释放,以便稍后在程序中使用。

student3 = null;
console.log(roster);
/**
WeakSet {Object {name: 'Julia', age: 27, gender: 'female'}, Object {name: 'James', age: 26, gender: 'male'}}
*/

这种机制的好处在于你不用去担心要删掉对 WeakSet 中已删除对象的引用,JavaScript 会帮你删除!如果对象被删除,当垃圾回收器运行时,该对象也会从 WeakSet 中删除。这样的话,如果你想要一种高效、轻便的解决方法去创建一组对象,就可以使用 WeakSet。垃圾回收的发生时间点取决于很多因素

Map

Map 和 WeakMap 在很多方面与 Set 和 WeakSet 相同,它们都有类似的属性和方法。Map 和 Set 都是可迭代的,这意味着我们可以循环遍历它们。而WeakMap 和 WeakSet 不会阻止对象被当作垃圾回收。但是 Map 是唯一的,因为它们是键值对的集合,而 Set 是唯一值的集合。可以说 Set 类似于数组,而Map 类似于对象

创建和修改 Map

本质上,Map 是一个可以存储键值对的对象,键和值都可以是对象、原始值或二者的结合。

创建 Map

const employees = new Map();
console.log(employees); // Map {}

修改 Map

.set() 方法添加键值

const employees = new Map();

employees.set('[email protected]', { 
    firstName: 'James',
    lastName: 'Parkes',
    role: 'Content Developer' 
});
employees.set('[email protected]', {
    firstName: 'Julia',
    lastName: 'Van Cleve',
    role: 'Content Developer'
});
employees.set('[email protected]', {
    firstName: 'Richard',
    lastName: 'Kalehoff',
    role: 'Content Developer'
});

console.log(employees);
/**
Map {'[email protected]' => Object {...}, '[email protected]' => Object {...}, '[email protected]' => Object {...}}
*/

.delete() 方法移除键值对

employees.delete('[email protected]');
employees.delete('[email protected]');
console.log(employees);
/**
Map {'[email protected]' => Object {firstName: 'James', lastName: 'Parkes', role: 'Course Developer'}}
*/

.clear() 方法从 Map 中删除所有键值对

employees.clear()
console.log(employees); // Map {}

如果成功地删除了键值对,.delete() 方法会返回 true,失败则返回 false.set() 如果成功执行,则返回 Map 对象本身。如果你使用 .set() 向 Map 中添加键已存在的键值对,不会收到错误,但是该键值对将覆盖 Map 中的现有键值对。

处理 Map

.has() 方法并向其传入一个键来检查 Map 中是否存在该键值对

const members = new Map();

members.set('Evelyn', 75.68);
members.set('Liam', 20.16);
members.set('Sophia', 0);
members.set('Marcus', 10.25);

console.log(members.has('Xavier')); // false
console.log(members.has('Marcus')); // true

.get() 方法传入一个键,检索 Map 中的值。

console.log(members.get('Evelyn')); // 75.68

循环访问 Map

三种方式循环访问:

  • 使用 Map 的默认迭代器循环访问每个键或值
  • 使用新的 for...of 循环来循环访问每个键值对
  • 使用 Map 的 .forEach() 方法循环访问每个键值对

使用 MapIterator
在 Map 上使用 .keys().values() 方法将返回新的迭代器对象,叫做 MapIterator。你可以将该迭代器对象存储在新的变量中,并使用 .next() 循环访问每个键或值。

// 访问 Map 的键
let iteratorObjForKeys = members.keys();
iteratorObjForKeys.next();// Object {value: 'Evelyn', done: false}
iteratorObjForKeys.next(); // Object {value: 'Liam', done: false}
// 等等
// 访问 Map 的值
let iteratorObjForValues = members.values();
iteratorObjForValues.next(); // Object {value: 75.68, done: false}
// 等等

使用 for…of 循环

for (const member of members) {
  console.log(member);
}
// 键值对会拆分为一个数组,第一个元素是键,第二个元素是值。
/**
 ['Evelyn', 75.68]
 ['Liam', 20.16]
 ['Sophia', 0]
 ['Marcus', 10.25]
*/

使用 forEach 循环

members.forEach((value, key) => console.log(value, key));
/**
'Evelyn' 75.68
 'Liam' 20.16
 'Sophia' 0
 'Marcus' 10.25
*/

WeakMap

WeakMap 和普通 Map 很像,但是具有以下关键区别:

  • WeakMap 只能包含对象作为键
  • WeakMap 无法迭代,意味着无法循环访问
  • WeakMap 没有 .clear() 方法
const book1 = { title: 'Pride and Prejudice', author: 'Jane Austen' };
const book2 = { title: 'The Catcher in the Rye', author: 'J.D. Salinger' };
const book3 = { title: 'Gulliver's Travels', author: 'Jonathan Swift' };

const library = new WeakMap();
library.set(book1, true);
library.set(book2, false);
library.set(book3, true);

console.log(library);
/**
WeakMap {Object {title: 'Pride and Prejudice', author: 'Jane Austen'} => true, Object {title: 'The Catcher in the Rye', author: 'J.D. Salinger'} => false, Object {title: 'Gulliver's Travels', author: 'Jonathan Swift'} => true}
*/
// 添加对象以外的内容作为键,系统将报错!
library.set('The Grapes of Wrath', false);
// Uncaught TypeError: Invalid value used as weak map key(…)

垃圾回收

book1 = null;
console.log(library);
/**
WeakMap {Object {title: 'The Catcher in the Rye', author: 'J.D. Salinger'} => false, Object {title: 'Gulliver’s Travels', author: 'Jonathan Swift'} => true}
*/

Promise-异步请求

简介

使用 JavaScript Promise 是处理异步请求的一种新方法,是对我们过去代码构建方式的一种改良。处理一个请求需要来回等待,造成大量的停机时间,能够在停机时间同时进行其它工作,并且通知我们请求处理完成,就是 Promise 在 JavaScript 中的作用。

使用 Promise

JavaScript Promise 是用新的 Promise 构造函数 - new Promise() 创建而成的。promise 使你能够展开一些可以异步完成的工作,并回到常规工作。创建 promise 时,必须向其提供异步运行的代码。将该代码当做参数提供给构造函数:

new Promise(function () {
    window.setTimeout(function createSundae(flavor = 'chocolate') {
        const sundae = {};
        // 请求冰淇淋
        // 得到锥形蛋筒
        // 加热冰淇淋
        // 舀一大勺到蛋筒里!
    }, Math.random() * 2000);
});

JavaScript 如何通知我们它已经完成操作,准备好让我们恢复工作?

它通过向初始函数中传入两个函数来实现这一点,通常我们将这两个函数称为 resolvereject

resolve 函数-请求完成

new Promise(function (resolve, reject) {
    window.setTimeout(function createSundae(flavor = 'chocolate') {
        const sundae = {};
        // 请求冰淇淋
        // 得到锥形蛋筒
        // 加热冰淇淋
        // 舀一大勺到蛋筒里!
        resolve(sundae);
    }, Math.random() * 2000);
});

当 sundae 被成功创建后,它会调用 resolve 方法并向其传递我们要返回的数据,在本例中,返回的数据是完成的 sundae。因此 resolve 方法用来表示请求已完成,并且成功完成了请求。

reject 函数-请求失败

如果请求存在问题,无法完成请求,那么我们可以使用传递给该函数的第二个函数。通常,该函数存储在一个叫做"reject"的标识符中,表示如果请求因为某种原因失败了,应该使用该函数

new Promise(function (resolve, reject) {
    window.setTimeout(function createSundae(flavor = 'chocolate') {
        const sundae = {};
        // 请求冰淇淋
        // 得到锥形蛋筒
        // 加热冰淇淋
        // 舀一大勺到蛋筒里!
        if ( /* iceCreamConeIsEmpty(flavor) */ ) {
            reject(`Sorry, we're out of that flavor :-(`);
        }
        resolve(sundae);
    }, Math.random() * 2000);
});

如果请求无法完成,则使用 reject 方法。注意,即使请求失败了,我们依然可以返回数据。

Promise构造函数需要一个可以运行的函数,运行一段时间后,将成功完成(使用 resolve 方法)或失败(使用 reject 方法)。当结果最终确定时(请求成功完成或失败),现在 promise 已经实现了,并且将通知我们,这样我们便能决定将如何对结果做处理。

Promise 立即返回对象

首先要注意的是,Promise 将立即返回一个对象。

const myPromiseObj = new Promise(function (resolve, reject) {
    // 圣代创建代码
});

该对象上具有一个 .then() 方法,我们可以让该方法通知我们 promise 中的请求成功与否。.then() 方法会接收两个函数:

  • 请求成功完成时要运行的函数
  • 请求失败时要运行的函数
mySundae.then(function(sundae) {
    console.log(`Time to eat my delicious ${sundae}`);
}, function(msg) {
    console.log(msg);
    self.goCry(); // 不是一个真正的方法
});

传递给 .then() 的第一个函数将被调用,并传入 Promise 的 resolve 函数需要使用的数据。这里,该函数将接收 sundae 对象。第二个函数传入的数据会在 Promise 的 reject 函数被调用时使用。

更多 Promise

Proxy-代理

JavaScript 代理会让一个对象代表另一个对象,来处理另一个对象的所有交互。代理可以直接处理请求,接收或发送目标对象数据,以及处理一大堆其他的事情。

创建 Proxy

使用 Proxy 构造函数 new Proxy();。Proxy 构造函数接收两个项目:

  • 它将要代理的对象
  • 包含将为被代理对象处理的方法列表的对象

第二个对象叫做处理器

创建 Proxy 的最简单方式是提供对象和空的 handler(处理器)对象

var richard = {status: 'looking for work'};
var agent = new Proxy(richard, {});

agent.status; // 返回 'looking for work'

上述代码并没有对 Proxy 执行任何特殊操作,只是将请求直接传递给源对象!如果我们希望 Proxy 对象截获请求,这就是 handler 对象的作用了!

让 Proxy 变得有用的关键是当做第二个对象传递给 Proxy 构造函数的 handler 对象。handler 对象由将用于访问属性的方法构成。

Get Trap(捕获器)

get 用来截获对属性的调用:

const richard = {status: 'looking for work'};
const handler = {
    get(target, propName) {
        console.log(target); // `richard` 对象,不是 `handler` 也不是 `agent`
        console.log(propName); // 代理(本例中为`agent`)正在检查的属性名称
    }
};
const agent = new Proxy(richard, handler);
agent.status; // 注销 richard 对象(不是代理对象!)和正在访问的属性的名称(`status`)

在上述代码中,handler 对象具有一个 get 方法(因为被用在 Proxy 中,所以将"function"(方法)称之为"trap"(捕获器))。当代码 agent.status; 在最后一行运行时,因为存在 get 捕获器,它将截获该调用以获得 status(状态)属性并运行 get 捕获器方法。这样将会输出 Proxy 的目标对象(richard 对象),然后输出被请求的属性(status 属性)的名称。它的作用就是这些!它不会实际地输出属性!这很重要 —— 如果使用了捕获器,你需要确保为该捕获器提供所有的功能

从 Proxy 内部访问目标对象

如果我们想真正地提供真实的结果,我们需要返回目标对象的属性:

const richard = {status: 'looking for work'};
const handler = {
    get(target, propName) {
        console.log(target);
        console.log(propName);
        return target[propName];
    }
};
const agent = new Proxy(richard, handler);
agent.status; //  (1)打印 richard 对象,(2)打印被访问的​​属性,(3)返回 richard.status 中的文本

get trap 中添加了最后一行 return target[propName];,这样将会访问目标对象的属性并返回它。

直接获取 Proxy 的返回信息

使用 Proxy 提供直接的反馈:

const richard = {status: 'looking for work'};
const handler = {
    get(target, propName) {
        return `He's following many leads, so you should offer a contract as soon as possible!`;
    }
};
const agent = new Proxy(richard, handler);
agent.status; // 返回文本 `He's following many leads, so you should offer a contract as soon as possible!`

对于上述代码,Proxy 甚至不会检查目标对象,直接对调用代码做出响应

因此每当 Proxy 上的属性被访问,get trap 将接管任务。如果我们想截获调用以更改属性,则需要使用 set trap!

set trap 用来截获将更改属性的代码。set trap 将接收: 它代理的对象 被设置的属性 Proxy 的新值

const richard = {status: 'looking for work'};
const handler = {
    set(target, propName, value) {
        if (propName === 'payRate') { // 如果工资正在确定,则需要15%作为佣金。
            value = value * 0.85;
        }
        target[propName] = value;
    }
};
const agent = new Proxy(richard, handler);
agent.payRate = 1000; // 将演员的工资设置为 1,000美元
agent.payRate; // 850美元是演员的实际工资

在上述代码中,注意 set trap 会检查是否设置了 payRate 属性。如果设置了,Proxy 就从中拿走 15% 的费用作为自己的佣金!当演员的薪酬是一千美元时,因为 payRate 属性已设置,代码从中扣除 15% 的费用,并将实际 payRate 属性设为 850;

其他 Trap

实际上总共有 13 种不同的 Trap,它们都可以用在处理程序中!

  1. get trap - 使 proxy 能处理对属性访问权的调用
  2. set trap - 使 proxy 能将属性设为新值
  3. apply trap - 使 proxy 能被调用(被代理的对象是函数)
  4. has trap - 使 proxy 能使用 in 运算符
  5. deleteProperty trap - 使 proxy 能确定属性是否被删除
  6. ownKeys trap - 使 proxy 能处理当所有键被请求时的情况
  7. construct trap - 使 proxy 能处理 proxy 与 new 关键字一起使用当做构造函数的情形
  8. defineProperty trap - 使 proxy 能处理当 defineProperty 被用于创建新的对象属性的情形
  9. getOwnPropertyDescriptor trap - 使 proxy 能获得属性的描述符
  10. preventExtenions trap - 使 proxy 能对 proxy 对象调用 Object.preventExtensions()
  11. isExtensible trap - 使 proxy 能对 proxy 对象调用 Object.isExtensible
  12. getPrototypeOf trap - 使 proxy 能对 proxy 对象调用 Object.getPrototypeOf
  13. setPrototypeOf trap - 使 proxy 能对 proxy 对象调用 Object.setPrototypeOf

Proxy 与 ES5 Getter/Setter

对于 ES5 的 getter 和 setter 方法,你需要提前知道要获取/设置的属性

var obj = {
    _age: 5,
    _height: 4,
    get age() {
        console.log(`getting the "age" property`);
        console.log(this._age);
    },
    get height() {
        console.log(`getting the "height" property`);
        console.log(this._height);
    }
};

对于上述代码,注意在初始化对象时,我们需要设置 get age()get height()。因此,当我们调用下面的代码时,将获得以下结果:

obj.age; // 打印 'getting the "age" property' 和 5
obj.height; // 打印 'getting the "height" property' 和 4

但是当我们向该对象添加新的属性时,并没有显示 ageheight 属性那样生成的 getting the "weight" property 消息。

obj.weight = 120; // 在对象上设置一个新的属性
obj.weight; // 只打印120

对于 ES6 中的 Proxy,我们不需要提前知道这些属性:

const proxyObj = new Proxy({age: 5, height: 4}, {
    get(targetObj, property) {
        console.log(`getting the ${property} property`);
        console.log(targetObj[property]);
    }
});

proxyObj.age; // 打印 'getting the age property' 和 5
proxyObj.height; // 打印 'getting the height property' 和 4

当我们添加新的属性时

proxyObj.weight = 120; // 在对象上设置一个新的属性
proxyObj.weight; // 打印 'getting the weight property' 和 120

因此 proxy 对象的某些功能可能看起来类似于现有的 ES5 getter/setter 方法,但是对于 proxy,在初始化对象时,不需要针对每个属性使用 getter/setter 初始化对象

Proxy 是一种强大的创建和管理对象之间的交互的新方式。

生成器

每当函数被调用时,JavaScript 引擎就会在函数顶部启动,并运行每行代码,直到到达底部。无法中途停止运行代码,并稍后重新开始。一直都是这种“运行到结束”的工作方式:

function getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        console.log(name);
    }

    console.log('the function has ended');
}

getEmployee();

运行上述代码将在控制台中输出以下内容:

the function has started
Amanda
Diego
Farrin
James
Kagure
Kavita
Orit
Richard
the function has ended

如果你想先输出前三名员工的姓名,然后停止一段时间,稍后再从停下的地方继续输出更多员工的姓名呢?普通函数无法这么做,因为无法中途“暂停”运行函数。

可暂停的函数

如果我们希望能够中途暂停运行函数,则需要使用 ES6 中新提供的一种函数,叫做 generator(生成器)函数!

function* getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        console.log( name );
    }

    console.log('the function has ended');
}

星号表示该函数实际上是生成器!尝试运行该函数

getEmployee();

// 这是我在 Chrome 中获得的回应:
getEmployee {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

注意,生成器的星号实际上可以放在 function 关键字和函数名称之间的任何位置

生成器和迭代器

生成器被调用时,它不会运行函数中的任何代码,而是创建和返回迭代器。该迭代器可以用来运行实际生成器的内部代码。

const generatorIterator = getEmployee();
generatorIterator.next();

关键字 yield

关键字 yield 是 ES6 中新出现的关键字。只能用在生成器函数中。yield 会导致生成器暂停下来。我们向我们的生成器中添加 yield

function* getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        console.log(name);
        yield;
    }

    console.log('the function has ended');
}

注意,现在 for...of 循环中出现了 yield。如果我们调用该生成器(生成迭代器),然后调用 .next(),将获得以下输出:

const generatorIterator = getEmployee();
generatorIterator.next();
/**
the function has started
Amanda
*/
generatorIterator.next(); // Diego
generatorIterator.next(); // Farrin
// ...

它能完全记住上次停下的地方!它获取到数组中的下一项(Diego),记录它,然后再次触发了 yield,再次暂停。

向外面的世界生成数据

function* getEmployee() {
    console.log('the function has started');

    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];

    for (const name of names) {
        yield name;
    }

    console.log('the function has ended');
}

注意,现在从 console.log(name); 切换成了 yield name;。做出这一更改后,当生成器运行时,它会把姓名从函数里返回出去,然后暂停执行代码。我们看看具体效果:

const generatorIterator = getEmployee();
let result = generatorIterator.next();
result.value // 是 "Amanda"

generatorIterator.next().value // 是 "Diego"
generatorIterator.next().value // 是 "Farrin"

示例

迭代器的 .next() 方法需要被调用多少次,才能完全完成/用尽下面的 udacity 生成器函数:

function* udacity() {
    yield 'Richard';
    yield 'James'
}

它被调用的次数将比生成器函数中的 yield 表达式的数量多一次

  1. .next() 的第一次调用将启动该函数,并运行为第一个 yield
  2. .next() 的第二次调用将从暂停的地方继续,并运行第二个 yield
  3. .next() 的第三次(即最后一次)调用将再次从暂停的地方继续,并运行到函数结尾处。

向生成器中发送数据或从中向外发送数据

我们可以使用关键字 yield 从生成器中获取数据。我们还可以将数据发送回生成器中。方式是使用 .next() 方法:

function* displayResponse() {
    const response = yield;
    console.log(`Your response is "${response}"!`);
}

const iterator = displayResponse();

iterator.next(); // 开始运行生成器函数
iterator.next('Hello Udacity Student'); // 将数据发送到生成器中
// 上面的一行打印到控制台:你的响应是 "Hello Udacity Student"!

使用数据调用 .next()(即 .next('Richard'))会将该数据发送到生成器函数中上次离开的地方。它会将 yield 关键字替换为你提供的数据。

关键字 yield 用来暂停生成器并向生成器外发送数据,然后 .next() 方法用来向生成器中传入数据。下面是使用这两种过程来一次次地循环访问姓名列表的示例:

function* getEmployee() {
    const names = ['Amanda', 'Diego', 'Farrin', 'James', 'Kagure', 'Kavita', 'Orit', 'Richard'];
    const facts = [];
    for (const name of names) {
        // yield *出* 每个名称并将返回的数据存储到 facts 数组中
        facts.push(yield name); 
    }
    return facts;
}

const generatorIterator = getEmployee();

// 从生成器中获取第一个名称
let name = generatorIterator.next().value;

// 将数据传入 *并* 获取下一个名称
name = generatorIterator.next(`${name} is cool!`).value; 

// 将数据传入 *并* 获取下一个名称
name = generatorIterator.next(`${name} is awesome!`).value; 

// 将数据传入 *并* 获取下一个名称
name = generatorIterator.next(`${name} is stupendous!`).value; 

// 你懂的
name = generatorIterator.next(`${name} is rad!`).value; 
name = generatorIterator.next(`${name} is impressive!`).value;
name = generatorIterator.next(`${name} is stunning!`).value;
name = generatorIterator.next(`${name} is awe-inspiring!`).value;

// 传递最后一个数据,生成器结束并返回数组
const positions = generatorIterator.next(`${name} is magnificent!`).value; 

// 在自己的行上显示每个名称及其描述
positions.join('\n'); 

示例

function* createSundae() {
    const toppings = [];

    toppings.push(yield);
    toppings.push(yield);
    toppings.push(yield);

    return toppings;
}

var it = createSundae();
it.next('hot fudge');
it.next('sprinkles');
it.next('whipped cream');
it.next();

注意,第一次调用 .next() 将初始化生成器,将在第一个 yield 位置暂停。第二次调用 .next() 将向该 yield 提供数据。

数数有多少个 yield,以及每次调用 .next() 时,数据是如何被传入的。

toppings 数组的最后一项将是 undefined。因为:

  1. 因为第一次调用 .next() 传入了一些数据,但是该数据没有存储在任何位置。
  2. 最后一次调用 .next() 应该会获得一些数据(空数据),因为生成到对 toppings.push() 的最后一次调用中。

生成器是强大的新型函数,能够暂停执行代码,同时保持自己的状态。生成器适用于一次一个地循环访问列表项,以便单独处理每项,然后再转到下一项。还可以使用迭代器来处理嵌套回调。例如,假设某个函数需要获得所有仓库的列表和被加星标的次数。在获得每个仓库的星标数量之前,需要获得用户的信息。获得用户的个人资料后,代码可以利用该信息查找所有的仓库。

你可能感兴趣的:(前端开发进阶)