Iterator Help 迭代器

内存占用

  • 原始的处理数据方法内存分析 假设数据的长度无限长,那么一定会 oom
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 执行 filter 时,开辟新数组,将符合条件的数据放到新数组,内存 * 2
// 执行 map 时,开辟新数组,将数据处理后,在存放到新数组,内存再次 * 2
const result = data.filter((it) => it).map((it) => it * 2);

迭代器示例

  • 迭代器解决办法
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = data
  .values() // 将数组转为迭代器
  .filter((it) => it) // 过滤,不开放新数组 内存 * 0(惰性求值)
  .map((it) => it * 2) // 返回数据处理,不开放新数组 内存 * 0(惰性求值)
  .take(data.length) // 取出前 x 条数据处理。
  .toArray(); // 将结果输出为 array
  • 在看个示例,会更好理解 take
const w = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

console.log(
  w
    .values()
    .filter((it) => it > 8)
    .take(2)
    .toArray(),
); // [ 9, 10 ] 因为只取出了 2条数据。

/**
 * 执行顺序:
 * 1. toArray 向take 要数据。
 * 2. take 拿个小本记一下,现在符合的数据 0 条。
 * 3. filter 执行,符合符合,则返回给take,take + 1,不符合,take + 0
 * 4. 当 take 发现 符合的数据已经为2了,中止执行,返回数据给 toArray。
 * **/
  • 发现问题了吗?如果符合的数据在数组的最后两条。那么仍然要完整的走完全部的流程。时间复杂度是一样的。 但是,对空间复杂度上,是不会滚雪球的。因为 map 和 filter 会产生中间数组,尤其是map,在执行时,发现原始数组的length 为多少,就会先开辟一个新的数组,内存长度为原始数组的长度。在程序执行过程中,把符合的数据放到中间数组中去。(也就是说,在执行阶段,中间数组就产生了一直到程序执行完成)Iterator 不会,其只会在执行完成后产生一个新的数组。在程序执行时,不会产生中间数组。
const w = [1, 2, 3, 4, 5, 6, 7, 8, { a: 1 }, { b: 2 }];

const qq = w
  .values()
  .filter((it) => it instanceof Object)
  .take(2)
  .toArray();

w.at(-1).b = "12312312";
console.log(qq, w); // [{ a: 1 }, { b: 12312312 }] 浅拷贝

时间复杂度和空间复杂度

[1, 2, 3, 4, 5].filter((it) => it > 2);

// 时间复杂度:O(n)   原因:filter 必须“逐一检查”。
// 空间复杂度:O(n)   原因:filter 必须“开辟新地”开辟的新的数组长度由原始数组长度决定

[1, 2, 3, 4, 5].values().filter((it) => it > 2);
// 时间复杂度:依然是 O(n)。因为你总归要检查完这 5 个数
// 空间复杂度:降到了 O(1)。迭代器本身只是一个很小的“指针对象”,它没有产生任何新数组。只有当你调用 .toArray() 时,空间复杂度才会重新回到 O(n)。但如果你直接用 for...of 消费它,空间就始终维持在 O(1)。

[1, 2, 3, 4, 5].values().filter((it) => it > 2);
// 时间复杂度:依然是 O(n)。大O表示法,考虑最坏的情况,数组全部过滤一遍
// 空间复杂度:O(1),不调用 toArray,其内存不会出现多余的占用。