目录
迭代器
-
什么是迭代?
答:按照顺序反复多次执行一段程序,通常有明确的终止条件
-
JavaScript为什么需要像Python、Java、C++新增了迭代器模式
答:需要事先知道如何使用数据结构,遍历顺序并不是数据结构固有的(比如对象的属性是无序的)
迭代器是通用的方法,不需要知道迭代对象的数据结构,只需要能按照一定顺序取得数据即可
什么是迭代器模式
特定结构的可迭代对象,对外暴露了Iterable(可迭代对象)接口,而且可以通过Iterator(迭代器)消费
可迭代对象是一种抽象的说法。基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序:
// 数组的元素是有限的
// 递增索引可以按序访问每个元素
let arr = [3, 1, 4];
// 集合的元素是有限的
// 可以按插入顺序访问每个元素
let set = new Set().add(3).add(1).add(4);
不过,可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,比如本章开头提到的计数循环。该循环中生成的值是暂时性的,但循环本身是在执行迭代。计数循环和数组都具有可迭代对象的行为。
任何实现 Iterable 接口的数据结构都可以被实现 Iterator 接口的结构“消费”(consume)。迭代器(iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。这种概念上的分离正是 Iterable 和 Iterator 的强大之处。
可迭代协议
ECMA规定,暴露一个属性作为迭代器,这个属性必须使用特殊的Symbol.iterator作为key。
以下内置类型都实现了Iterable接口,1.字符串、2.数组、3.映射、4.集合、5.arguments对象、6.NodeList等Dom集合类型。
以下代码仅演示和理解迭代器,实际开发不需要这样操作。
let num = 1;
let obj = {};
// 这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined
let str = 'abc';
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c');
let els = document.querySelectorAll('div');
// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] }
console.log(arr[Symbol.iterator]); // f values() { [native code] }
console.log(map[Symbol.iterator]); // f values() { [native code] }
console.log(set[Symbol.iterator]); // f values() { [native code] }
console.log(els[Symbol.iterator]); // f values() { [native code] }
// 调用这个工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]()); // StringIterator {}
console.log(arr[Symbol.iterator]()); // ArrayIterator {}
console.log(map[Symbol.iterator]()); // MapIterator {}
console.log(set[Symbol.iterator]()); // SetIterator {}
console.log(els[Symbol.iterator]()); // ArrayIterator {}
以下方法可以直接使用上面这些可迭代对象,1.for-of、2.数组解构、3.扩展运算符、4.Array.from()、5.创建集合、6.创建集合和映射、7.Promise.all()和Promise.race()、8.在generator生成器中使用yield*操作符、
let arr = ['a','b','c']
// for-of循环
for (const el of arr) { console.log(el) } // a b c
// 数组解构
let [x,y,z] = arr
console.log(x,y,z); // a b c
// 扩展运算符
let arr2 = [...arr]
console.log(arr2); // ['a', 'b', 'c']
// Array.from()
let arr3 = Array.from(arr)
console.log(arr3); // ['a', 'b', 'c']
// 集合和映射
let set = new Set(arr)
console.log(set); // Set(3) {'a', 'b', 'c'}
let pairs = arr.map((x,i) => [x,i])
let map = new Map(pairs)
console.log(map); // Map(3) {'a' => 0, 'b' => 1, 'c' => 2}
迭代器协议
迭代器是一种一次性使用的对象。迭代器API使用next()方法,返回一个IterableResult对象,包含两个属性键名为done和value。
每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例相互之间没有联系,只会独
立地遍历可迭代对象:
let arr = ['a', 'b'] // 可迭代对象
console.log(arr[Symbol.iterator]); // 迭代器工厂函数,ƒ values() { [native code] }
let iterFirst = arr[Symbol.iterator]() // 调用工厂函数生成的第一个迭代器
let iterSecond = arr[Symbol.iterator]() // 生成的第二个迭代器
console.log(iterFirst.next()); // {value: 'a', done: false}
console.log(iterSecond.next()); // {value: 'a', done: false}
console.log(iterFirst.next()); // {value: 'b', done: false}
console.log(iterSecond.next()); // {value: 'b', done: false}
console.log(iterSecond.next()); // {value: undefined, done: true}
console.log(iterSecond.next()); // {value: undefined, done: true}
console.log(iterSecond.next()); // {value: undefined, done: true}
迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。 如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化:
let arr = ['foo', 'baz'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { done: false, value: 'foo' }
// 在数组中间插入值
arr.splice(1, 0, 'bar');
console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: false, value: 'baz' }
console.log(iter.next()); // { done: true, value: undefined }
- 不同迭代器之间不会相互影响,可以独立的迭代对象
- done为一个布尔值只要done为false,则可以继续调用next()方法。value为迭代器返回的当前值。
- 当done为true时,后续调用next()value为undefiend
自定义迭代器
class Counter {
constructor(limit) {
this.limit = limit
}
[Symbol.iterator]() { // 定义[Symbol.iterator]方法,
let count = 1, // 这里通过闭包达到一个效果,即使同时创建多个迭代器时也保证计算器count从1 开始(闭包作用之一,避免全局变量count被污染)
limit = this.limit
return {
next() { // 对迭代器使用next()方法
if (count <= limit) {
return { done: false, value: count++ }
} else {
return { done: true, value: undefined }
}
}
}
}
}
let counter = new Counter(5)
for (const el of counter) { console.log(el); } // 1 2 3 4 5
提前终止迭代器
ES5中提出的forEach是没有终止的(除了trycatch),return()方法可以提前终止迭代。
- for-of循环通过break,continue,return或throw提前退出
- 解构操作并未消费所有值
生成器
-
什么是生成器?
生成器本质是函数,在函数名称前加一个星号(*)表示为生成器,星号不受两侧空格的影响。调用之后返回一个生成器对象。
-
生成器有什么用?
生成器是ES6新增的结构,拥有在一个函数块内暂停和恢复代码执行的能力。生成器可以自定义迭代器。
生成器函数的定义
<script>
function* generatorFn() { }
let generatorFn = function* () { }
let foo = {
* generatorFn() { }
}
class Foo {
* generatorFn() { }
}
class Bar {
static * generatorFn() { }
}
生成器的调用与执行
function* generatorFn() {
console.log('foo');
return 'foo'
};
const result = generatorFn();
console.log(result); // generatorFn {<suspended>},调用生成器函数产生一个生成器对象(暂停执行),直接调用生成器函数并不会执行内部代码
console.log(result.next); // ƒ next() { [native code] },和迭代器一样有next方法
console.log(result.next()); // 控制台打印foo {value: 'foo', done: true},生成器函数只会在初次调用next()方法开始执行,执行内部代码,返回结果类似迭代器
console.log(result.next()); // {value: undefined, done: true},
yield关键字中断执行
yield关键字可以控制生成器的停止和开始执行。遇到这个关键字,执行停止。停止后只能通过next()方法恢复执行。
function* generatorFn() {
console.log('a');
yield 'foo'
console.log('b');
yield 'bar'
console.log('c');
return 'baz'
console.log('d');
};
const result = generatorFn();
console.log(result.next()); // a {value: 'foo', done: false} 遇到yield关键字退出的生成器函数会处在done:false状态。
console.log(result.next()); // b {value: 'bar', done: false}
console.log(result.next()); // c {value: 'baz', done: true} 遇到return关键字退出的生成器函数会处在done:true状态。
console.log(result.next()); // {value: undefined, done: true} done的状态已经转变为true,所以字符串d并不会打印
ield关键字使用的注意点
- 和迭代器一样,调用同一个生成器函数生成多个生成器对象,各个对象之间不会影响
- yield关键字只允许直接在生成器函数内部使用,并且不允许嵌套层级。否在会无效
直接显式的调用next()的方法用处不大,如果把生成器对象当成可迭代对象,使用起来会更方便。
function* generatorFn() {
yield 1
yield 2
yield 3
}
for (const iterator of generatorFn()) {
console.log(iterator); // 1 2 3
}
.使用yield实现输入和输出
生成器传递参数
function* generatorFn(init) {
console.log(init);
console.log(yield);
console.log(yield);
}
let result = generatorFn('foo')
result.next('bar') // foo 第一次调用next传入的值不会被使用,因为这次调用是为了开始执行生成器函数
result.next('baz') // baz
result.next('qux') // qux
// 迭代索引
function* nTimes(n) {
for (let i = 0; i < n; i++) {
yield i
}
}
for (const iterator of nTimes(3)) {
console.log(iterator); // 0 1 2
}
// 实现范围输出
function* range(start, end) {
while (end > start) {
yield start++
}
}
for (const iterator of range(4, 8)) {
console.log(iterator); // 4 5 6 7
}
// 实现填充数组
function* fill(n) {
while (n--) {
yield 0
}
}
console.log(Array.from(fill(8))); // [0, 0, 0, 0, 0, 0, 0, 0]
使用yield*可以一次性迭代可迭代对象
yield* 本质是语法糖,yield*本质上将一个可迭代对象序列化为一连串可以单独产出的值,所以等同于把yield放入循环。
// 这种写法可以直接用 yield* 来代替
// function* generatorFn() {
// for (const iterator of [1, 2, 3]) {
// yield iterator
// }
// }
function* generatorFn() {
yield* [1, 2, 3]
yield* [4, 5]
yield* [6, 7]
}
let result = generatorFn()
for (const iterator of generatorFn()) {
console.log(iterator); // 1,2,3,4,5,6,7
}
yield*关联迭代器返回done:true的value属性,对普通迭代器这个值为undefined。而对于生成器函数来说,这个值就是生成器函数返回的值。
function* generatorFn() {
console.log('iter value', yield* [1, 2, 3]); // yield* 关联普通迭代器
}
for (const iterator of generatorFn()) {
console.log('value', iterator);
}
/*
value 1
value 2
value 3
iter value undefined
*/
function* innerGeneratorFn() {
yield 'foo'
return 'bar'
}
function* outerGeneratorFn() {
console.log('iter value:', yield* innerGeneratorFn()); // yield* 关联生成器函数
}
for (const iterator of outerGeneratorFn()) {
console.log('value', iterator);
}
/*
value foo
iter value: bar
*/
提前终止生成器
与迭代器类似,生成器也支持可关闭的概念。一个实现Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。除了以上方法还有throw()
return()
return()方法会强制生成器进入关闭状态。return 传入的值就是终止迭代器对象的值
function* generatorFn() {
yield* [1, 2, 3]
}
const result = generatorFn()
console.log(result); // generatorFn {<suspended>}
// console.log(result.return(4)); // {value: 4, done: true}
console.log(result); // generatorFn {<closed>}
console.log(result.next()); // {value: undefined, done: true}
console.log(result.next()); // {value: undefined, done: true}
console.log(result.next()); // {value: undefined, done: true}
throw()
throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。
const result = generatorFn()
console.log(result); // generatorFn {<suspended>}
try {
result.throw('error')
} catch (error) {
console.log(error); // error
}
console.log(result); // generatorFn {<closed>}
不过,假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的yield。
function* generatorFn() {
yield* [1, 2, 3]
}
function* generatorFn() {
for (const iterator of [1, 2, 3]) {
try {
yield iterator
} catch (error) { }
}
}
const result = generatorFn()
console.log(result.next()); // {value: 1, done: false}
result.throw('foo')
console.log(result.next()); // {value: 3, done: false}