ECMAScript 的 Iterator Helper 提案正式获得浏览器支持!

2024-04-15 16:33:42 浏览数 (3)

大家好,我是 ConardLi

相信 Iterator(迭代器)这个概念大家并不陌生了,它和数组的概念类似,在 JavaScript 中都是用于存储和管理数据集合的机制。

但实际开发中,我们使用数组的场景要远远多于 Iterator ,主要原因还是因为 Iterator 太难用了,它不像数组一样给我们提供了很多便捷的高阶函数(如 map、filter 等) 。

Iterator helpers 提案正式出来解决这个问题的,它已经有几年时间了,目前处于 Stage3 阶段。

Iterator helpers 提供了一整套方法,使得迭代器的操作变得像数组一样简单。它允许你可以以链式调用的方式来组合方法,比如可以先用 .map() 处理数据,紧接着用 .filter() 筛选出需要的部分,最后用 .toArray() 将其转换成数组。

最近在 V8 12.2/Chrome 122 中,Iterator helpers 已经正式获得了支持。

在开始介绍之前,我们先看看 Iterator 和数组的区别,再实际开发中,我们在什么场景下更适合使用 Iterator

Iterator 和数组的对比

  1. 计算模式:
    • 数组是静态的: 数组在创建时就包含了一个固定大小的数据集合。你可以立即访问数组的任何元素,因为它们都是预先存储在内存中的。
    • 迭代器是惰性的(Lazy): Iterator 不必一开始就拥有所有的数据。它每次调用 next() 方法时才计算出下一个值。这意味着它可以表示无限的数据序列,并且可以按需产生数据,而不需要一开始就将所有数据加载到内存中。
  2. 性能和内存占用:
    • 数组可能占用更多内存: 因为需要预先存储所有元素。
    • 迭代器更高效: 它们可以在不占用大量内存的情况下,遍历巨大的甚至是无限的数据集。
  3. 使用场景的不同:
    • 数组用于存储元素集: 当你需要随机访问、多次遍历或者需要大量的数据操作时,使用数组是比较好的选择。
    • 迭代器用于遍历元素: 当数据集不需要一次性全部存储在内存中,或者希望按需计算每个值时,迭代器更为合适。

那么为啥有了使有了数组,我们还要还要用到 Iterator 呢?

  • 对于巨大或不确定大小的数据集, 迭代器可以有效地按需处理数据。例如,在处理文件流或网络请求等情况时,使用迭代器可以在数据到达时逐步处理,而不必等待所有数据都准备好。
  • 基于顺序的操作和管道(Pipelines): 当数据需要一系列的操作来转换时,迭代器使得这些转换可以按顺序进行,这类似于函数式编程中的管道机制。

实际开发中,下面这些可能会是使用到 Iterator 的例子:

  • 处理大型数据集: 当你需要处理大量数据时,比如从数据库读取数百万条记录,使用迭代器可以避免一次性将所有数据加载到内存中。
  • 与生成器配合进行复杂计算: 生成器提供了一种方便编写迭代逻辑的方法,当计算每个值代价昂贵或需要保持状态时,它们非常有用。
  • 异步操作: 在处理异步数据流,如读取网络资源时,异步迭代器使得按顺序处理异步事件成为可能。
  • 前端框架和库: 许多现代前端框架和库利用迭代器来处理或渲染列表和组件,提供更高效的数据更新和渲染策略。

聊完了 Iterator 和数组的区别,我们下面来看看 Iterator helpers 都提供了哪些方法?

.map(mapperFn)

类似数组的 map 方法,map 方法接受一个映射函数作为参数,在函数中我们可以对原本的参数进行处理,最中返回一个新的迭代器:

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 获取文章列表,返回他们的文本内容(标题)列表并且输出。
for (const post of posts.values().map((x) => x.textContent)) {
  console.log(post);
}

.filter(filtererFn)

类似数组的 filter 方法,filter 方法接受一个过滤器函数作为参数,根据我们自定义的逻辑过滤掉一些不需要的元素,然后返回一个新的迭代器。

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 过滤出包含 `ConardLi` 的博客文章的文本内容(标题),并在控制台输出它们。
for (const post of posts.values().filter((x) => x.textContent.includes('ConardLi'))) {
  console.log(post);
}

.take(limit)

take 方法接受一个整数作为参数,返回一个迭代器中前几个参数组成的新的迭代器。

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 选择最近的 17 篇博客文章,并在控制台输出它们。
for (const post of posts.values().take(17)) {
  console.log(post);
}

.drop(limit)

drop 方法接受一个整数作为参数,返回从原始迭代器中排除前 n 个元素后的新的的迭代器。

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 排除最近的 17 篇博客文章,打印新的迭代器
for (const post of posts.values().drop(17)) {
  console.log(post);
}

.flatMap(mapperFn)

flatMap() 方法可以看作是 map()flat() 的结合体。

首先,map() 方法会遍历迭代器的每个元素,并将元素通过一个函数进行处理,最后返回一个新的迭代器。然后,flat() 方法可以用来展平迭代器,也就迭代器迭代器的维度。将二维迭代器变为一维迭代器迭代器。

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 获取每篇博客文章的标签列表,并且拍平后返回
for (const tag of posts.values().flatMap((x) => x.querySelectorAll('.tag').values())) {
    console.log(tag.textContent);
}

.reduce(reducer [, initialValue ])

reduce 方法接受一个 reducer 函数以及一个可选的初始值作为参数。

"reducer" 函数有两个参数:累积器和当前值。在每次迭代中,累积器的值是上一次调用 "reducer" 函数的结果,当前值则是数组中正在处理的元素。

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 获取所有文章的标签列表。
const tagLists = posts.values().flatMap((x) => x.querySelectorAll('.tag').values());

// 获取列表中每个标签的文本内容。
const tags = tagLists.map((x) => x.textContent);

// 统计带有 ConardLi 标签的文章数。
const count = tags.reduce((sum , value) => sum   (value === 'ConardLi' ? 1 : 0), 0);
console.log(count);

.toArray()

toArray() 方法可以将迭代器的值转换为一个数组。

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 从最近的10篇博客文章列表中创建一个数组。
const arr = posts.values().take(10).toArray();

.forEach(fn)

类似数组的 forEach() 方法,forEach() 方法接受一个函数作为参数,然后在迭代器的每一个元素上调用这个函数。这个函数执行的是带有副作用的操作,会改变原本的迭代器,它不返回任何值。

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 获取发布了至少一篇博客文章的日期并记录下来。
const dates = new Set();
const forEach = posts.values().forEach((x) => dates.add(x.querySelector('time')));
console.log(dates);

.some(fn)

类似数组的 some() 方法,some() 方法接受一个断言函数作为参数。如果在应用该函数后,有任何一个迭代器的元素返回 true,那么这个方法就会返回 true

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 查找任何博客文章的文本内容(标题)是否包含 `ConardLi` 关键字。
posts.values().some((x) => x.textContent.includes('ConardLi'));

.every(fn)

类似数组的 every() 方法,every() 方法接受一个断言函数作为参数。如果在应用该函数后,迭代器的每个元素都返回 true,那么这个方法就会返回 true

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 检查所有博客文章的文本内容(标题)是否全部包含 `ConardLi` 关键词。
posts.values().every((x) => x.textContent.includes('ConardLi'));

.find(fn)

类似数组的 find() 方法,find() 方法接受一个断言函数作为参数。然后其会返回迭代器中第一个使函数返回 true 的元素,如果没有任何一个元素满足条件,那么返回 undefined

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 打印最新的博客文章中包含 `ConardLi` 关键词的文本内容(标题)。
console.log(posts.values().find((x) => x.textContent.includes('ConardLi')).textContent);

Iterator.from(object)

from() 是一个静态方法,接受一个对象作为参数。如果该对象已经是迭代器的实例,那么这个方法会直接返回它。如果该对象具有 Symbol.iterator 属性,意味着它是可迭代的,那么就会调用它的 Symbol.iterator 方法来获取迭代器,并由此方法返回。否则,会创建一个新的迭代器对象(该对象从 Iterator.prototype 继承并具有 next()return() 方法),该对象包装了这个对象并由此方法返回。

代码语言:javascript复制
// 从博客存档页面中选择博客文章列表
const posts = document.querySelectorAll('li:not(header li)');

// 首先从帖子中创建一个迭代器。然后,记录包含 `ConardLi` 关键词的最新博客文章的文本内容(标题)。
console.log(Iterator.from(posts).find((x) => x.textContent.includes('ConardLi')).textContent);

最后

抖音前端架构团队目前放出不少新的 HC ,又看起会的小伙伴可以看看这篇文章:抖音前端架构团队正在寻找人才!FE/Client/Server/QA

0 人点赞