JavaScript中的迭代及相关问题

2020/12/15 JavaScript

# 可迭代协议和迭代器协议

ES2015补充规范:可迭代协议 (opens new window)迭代器协议 (opens new window)

# 可迭代协议

允许JavaScript对象定义或定制它们的迭代行为,例如,在一个for...of结构中,哪些值可以被遍历到:

内置可迭代对象:StringArrayTypedArrayMapSetObject是不可迭代的,因此加入了Map来替代

要成为可迭代对象,一个对象必须实现@@iterator方法,这意味着对象(或者它原型链上的某个对象)必须有一个键为@@iterator的属性,可通过常量Symbol.iterator访问该属性,返回值是一个无参数的函数,其返回值为一个符合迭代器协议的对象

String.prototype[Symbol.iterator]; // ƒ [Symbol.iterator]() { [native code] }
Array.prototype[Symbol.iterator]; // ƒ values() { [native code] }
Map.prototype[Symbol.iterator]; // ƒ entries() { [native code] }
Set.prototype[Symbol.iterator]; // ƒ values() { [native code] }
Object.prototype[Symbol.iterator]; // undefined
1
2
3
4
5

# 迭代器协议

定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值

只有实现了一个拥有以下语义(semantic)的next()方法,一个对象才能成为迭代器,next()是一个无参数函数,返回一个应当拥有以下两个属性的对象:

  1. done(boolean)

    • 如果迭代器可以产生序列中的下一个值,则为false(这等价于没有指定done这个属性)
    • 如果迭代器已将序列迭代完毕,则为true,这种情况下,value是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
  2. value

    • 迭代器返回的任何JavaScript值。donetrue时可省略。

    next()方法必须返回一个对象,该对象应当有两个属性:donevalue,如果返回了一个非对象值(比如falseundefined),则会抛出一个TypeError异常("iterator.next() returned a non-object value"

迭代协议的使用:

const iterator = ('hi')[Symbol.iterator]();
iterator.next(); // { value: "h", done: false }
iterator.next(); // { value: "i", done: false }
iterator.next(); // { value: undefined, done: true }

// 重写可迭代协议的迭代行为
// 必须构造 String 对象以避免字符串字面量 auto-boxing
const someString = new String('hi');
someString[Symbol.iterator] = function () {
  return { // 只返回一次元素, 字符串 "bye", 的迭代器对象
    next: function() {
      if (this.first) {
        this.first = false;
        return { value: 'bye', done: false };
      }
      return { done: true };
    },
    first: true,
  };
};

[...someString]; // ['bye']
// 如果使用 next() 则会输出 {value: 'bye', done: false} {done: true}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

接受可迭代对象的内置API

new Map([[1, 'a'], [2, 'b'], [3, 'c']]).get(2); // "b"

const myObj = {};

new WeakMap([
  [{}, 'a'],
  [myObj, 'b'],
  [{}, 'c'],
]).get(myObj); // "b"

new Set([1, 2, 3]).has(3); // true
new Set('123').has('2'); // true

new WeakSet(function* () {
  yield {};
  yield myObj;
  yield {};
}()).has(myObj); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Map,WeakMap,Set,WeakSet的区别

  • MapSet为强引用,键、值为任意类型。不能进行垃圾回收,均可使用forEachfor...of遍历
  • Map是用数组来存取键和值,所以在设置值和取值的时候的时间复杂度都是O(n)
  • WeakMapWeakSet为弱引用,可以进行垃圾回收。WeakMap键必须为对象,WeakSet值必须为对象
  • WeakMap由于是弱引用,所以无法枚举所有的key,因此是无法进行遍历的。WeakSet同理不能遍历

迭代器实例

function makeIterator(array) {
  let nextIndex = 0;
  return {
    next() {
      return nextIndex < array.length ? {
        value: array[nextIndex++],
        done: false,
      } : {
        done: true,
      };
    },
  };
}

const it = makeIterator(['哟', '呀']);
console.log(it.next().value); // '哟'
console.log(it.next().value); // '呀'
console.log(it.next().done); // true

// 使用生成器
function* makeSimpleGenerator(array) {
  let nextIndex = 0;
  while (nextIndex < array.length) {
    yield array[nextIndex++];
  }
}
const gen = makeSimpleGenerator(['哟', '呀']);
console.log(gen.next().value); // '哟'
console.log(gen.next().value); // '呀'
console.log(gen.next().done); // true

// 生成器有next方法, 所以是一个迭代器, 并且有 [Symbol.iterator] 返回一个函数 所以是一个可迭代对象
// 所以生成器对象既是迭代器, 也是可迭代对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 生成器与yield

function*这种声明方式(function关键字后跟一个星号)会定义一个生成器函数(generator function),它返回一个Generator对象。也可以使用构造函数GeneratorFunctionfunction*expression定义生成器函数。

生成器函数不能当构造器使用

function* name([param[, param[, ... param]]]) { statements }

// function*
function* generator(i) {
  yield i;
  yield i + 10;
}
const gen = generator(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 20
console.log(gen.next().value); // undefined 因为只会返回两个值 第三次 done为true

// GeneratorFunction 并不是全局对象
Object.getPrototypeOf(function*(){}).constructor
new GeneratorFunction ([arg1[, arg2[, ...argN]],] functionBody)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

生成器函数在执行时能暂停,后面又能从暂停处继续执行。调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器(iterator)对象。当这个迭代器的next()方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield的位置为止,yield后紧跟迭代器要返回的值。或者如果用的是yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。

next()方法返回一个对象,这个对象包含两个属性:valuedonevalue属性表示本次yield表达式的返回值,done属性为布尔类型,表示生成器后续是否还有yield语句,即生成器函数是否已经执行完毕并返回。

调用next()方法时,如果传入了参数,那么这个参数会传给上一条执行的yield语句左边的变量

function* gen() {
  yield 10;
  x = yield 'foo';
  yield x;
}
const genObj = gen();
console.log(genObj.next()); // 执行 yield 10, 返回 10
console.log(genObj.next()); // 执行 yield 'foo', 返回 'foo'
console.log(genObj.next(100)); // 将 100 赋给上一条 yield 'foo' 的左值, 即执行 x=100, 返回 100
console.log(genObj.next()); // 执行完毕, value 为 undefined, done 为 true
1
2
3
4
5
6
7
8
9
10

当在生成器函数中显式return时,会导致生成器立即变为完成状态,即调用next()方法返回的对象的donetrue。如果return后面跟了一个值,那么这个值会作为当前调用next()方法返回的value值。

// yield* 转移
function* anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}
function* generator(i) {
  yield i;
  yield* anotherGenerator(i); // 移交执行权
  yield i + 10;
}

const gen = generator(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12
console.log(gen.next().value); // 13
console.log(gen.next().value); // 20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

深入理解js中的yield (opens new window)

  • yield并不能直接生产值,而是产生一个等待输出的函数
  • 除IE外,其他所有浏览器均可兼容(包括win10的Edge)
  • 某个函数包含了yield,意味着这个函数已经是一个Generator
  • 如果yield在其他表达式中,需要用()单独括起来
  • yield表达式本身没有返回值,或者说总是返回undefined(由next返回)。因此在赋值时,变量的值始终来自于下一次的next()调用参数
  • next()可无限调用,但既定循环完成之后总是返回undefined,除非有显示的return,则返回return后跟的值

进阶:手写async,await

最近更新: 2023年03月21日 14:47:21