介绍
您是否曾经需要遍历列表,但该操作需要花费大量时间才能完成?您是否曾经因为操作占用过多内存而导致程序崩溃?当我尝试实现一个生成素数的函数时,这发生在我身上。
生成多达 100 万个素数所花费的时间比我希望的要多得多。但是产生高达 1000 万的数字是不可能的。我的程序会崩溃或挂起。我已经在使用 Eratosthenes 的筛子,它在生成素数方面应该比蛮力方法更有效。
如果您发现自己处于类似情况,您可以尝试使用不同的算法。有些搜索和排序算法在较大的输入上效果更好。缺点是这些算法可能更难立即掌握。另一种选择是使用不同的编程语言。
编译语言可能能够更快地处理代码。但是使用另一种语言可能不切实际。您也可以尝试使用多个线程。再一次,它可能不实用,因为您的编程语言必须支持这一点。
幸运的是,有了javascript,还有另一种选择。如果您有一项计算密集型任务,您可以使用迭代器和生成器来获得一些效率。迭代器是某些 JavaScript 集合的属性。
迭代器通过让您一次一个地使用列表中的项目来提高效率,就好像它们是一个流一样。生成器是一种可以暂停执行的特殊函数。调用生成器允许您一次生成一个数据块,而无需先将其存储在列表中。
迭代器
首先,让咱们回顾一下在 javaScript 中循环遍历集合的不同方式。表单的循环for (initial; condition; step) { ... }
将执行其主体中的命令特定次数。同样,只要条件为真,while 循环就会执行其主体中的命令。
您可以使用这些循环通过在每次迭代时递增索引变量来遍历列表。迭代是循环体的执行。这些循环不知道列表的结构。它们充当计数器。
for/in
循环和for/of
循环旨在迭代特定的数据结构。遍历数据结构意味着您正在逐步遍历其每个元素。for/in
循环遍历纯 JavaScript 对象中的键。循环遍历可for/of
迭代对象的值。什么是可迭代的?简单地说,可迭代对象是具有迭代器的对象。可迭代的示例是数组和集合。迭代器是对象的一个属性,它提供了一种遍历对象的机制。
迭代器的特别之处在于它如何遍历集合。其他循环需要预先加载整个集合以便对其进行迭代,而迭代器只需要知道集合中的当前位置。
您可以通过调用迭代器的 next 方法访问当前项。下一个方法将返回当前项目的值和一个布尔值,以指示您何时到达集合的末尾。以下是从数组创建迭代器的示例。
const alpha = ['a','b','c']; const it = alpha[Symbol.iterator](); it.next(); //{ value: 'a', done: false } it.next(); //{ value: 'b', done: false } it.next(); //{ value: 'c', done: false } it.next(); //{ value: undefined, done: true }
您还可以使用循环迭代迭代器的值for/of
。当您知道要访问对象中的所有项目时,请使用此方法。这就是您将如何使用循环遍历上一个列表的方式:
for (const elem of it){ console.log(elem); }
为什么要使用迭代器?当处理列表的计算成本很高时,使用迭代器是有益的。如果您有一个非常大的数据源,如果您尝试对其进行迭代,可能会导致程序出现问题,因为必须加载整个集合。
使用迭代器,您可以分块加载数据。这更有效,因为您只操作您需要的列表部分,而不会产生处理整个列表的额外成本。
例如,您已经从文件或数据库中加载了数据,并且您希望逐步在屏幕上显示信息。您可以从数据创建一个迭代器,并设置一个事件处理程序以在每次事件发生时抓取一些项目。这是一个这样的实现可能看起来像的例子:
let posts = load(url); let it = posts[Symbol.iterator](); function loadPosts(iterable, count) { for (let i = 0; i < count; i++) { display(iterable.next().value); } } document.getElementById('btnNext').onclick = loadPosts(it, 5);
发电机
如果你想建立一个集合,你可以用一个生成器来做。生成器函数可以通过在每次迭代时暂停执行来一次返回一个值。当您创建生成器的实例时,可以使用迭代器访问这些项目。这是创建生成器函数的一般语法:
function *genFunc() { ... yield value; }
*
表示这是一个生成器函数。关键字暂停咱们的yield
函数并提供生成器在特定时刻的状态。为什么要使用发电机?当您想通过算法在集合中生成一个值时,您将使用生成器。如果您有一个非常大或无限的集合,它特别有用。让咱们看一个例子来了解这对咱们有什么帮助。
假设您已经构建了一个在线台球游戏,并且您希望将玩家与游戏室进行匹配。您的目标是生成可以从 2,000 名玩家列表中选择两个不同玩家的所有方式。从列表中生成的两人组合['a', 'b', 'c', 'd']
将是ab, ac, ad, bc, bd, cd
。这是使用嵌套循环的解决方案:
function combos(list) { const n = list.length; let result = []; for (let i = 0; i < n - 1; i++) { for (let j = i + 1; j < n; j++) { result.push([list[i], list[j]]); } } return result; } console.log(combos(['a', 'b', 'c', 'd']));
现在尝试使用包含 2,000 个元素的列表执行该函数。(您可以使用将数字 1 到 2,000 添加到数组中的 for 循环来初始化列表)。现在运行代码时会发生什么?
当我在在线编辑器中运行代码时,网页崩溃。当我在 chrome 的控制台中尝试时,我可以看到输出缓慢打印出来。但是,我的计算机的 CPU 开始超速运行,我必须强制退出 Chrome。这是使用生成器函数的修改后的代码:
function *combos(list) { const n = list.length; for (let i = 0; i < n - 1; i++) { for (let j = i + 1; j < n; j++) { yield [list[i], list[j]]; } } } let it = combos(['a', 'b', 'c', 'd']); it.next();
另一个例子是,如果咱们想在斐波那契数列中生成无穷大的数字。这是一种实现:
function *fibGen() { let current = 0; let next = 1; while(true) { yield current; let nextNum = current + next; current = next; next = nextNum; } } let it = fibGen(); it.next().value; //0 it.next().value; //1 it.next().value; //1 it.next().value; //2
通常,无限循环会使您的程序崩溃。该fibGen
函数能够永远运行,因为没有停止条件。但是由于它是一个生成器,因此您可以控制执行每个步骤的时间。
审查
当您想要增量处理集合时,迭代器和生成器会派上用场。您可以通过跟踪集合的状态而不是集合中的所有项目来提高效率。集合中的项目一次评估一个,集合其余部分的评估延迟到以后。
迭代器提供了一种有效的方式来遍历和操作大型列表。生成器提供了一种有效的方式来构建列表。当您要使用复杂算法或实施并行编程来优化代码时,您应该尝试这些技术。