让我们考虑一下 JavaScript 中
map()
函数的简单使用:
[1,2].map(x => x + 1).map(x => x + 2); // [3,4]
我想知道每个箭头函数调用是否在单独的循环中执行,或者这些函数是否在一个循环中应用于数组元素。我知道,例如在 java 中,这些函数将在一个循环中执行(使用 Java 8 中的 Streams)。在这种情况下,我们可以链接一些非终端函数(它们返回 Stream,我们可以在其中调用另一个函数),但这些操作只有在我们使用终端函数(例如将 Stream 转换为某个集合的
collect()
时才会执行)一个数组)。因此,所有操作(地图、过滤器等)都被合并,以便所有操作都可以在一个循环中运行。
另一方面,我们有一个 JavaScript,我们知道
Array.prototype.map()
返回另一个数组,所以看起来每个 map()
调用都是在单独的循环中执行的。但这会浪费执行时间!最简单的答案是,它都是由引擎优化的,所以函数确实是在一个循环中执行的,但我找不到任何信息表明情况是这样。
一个附带问题 - 我们可以说 Java 的
Stream
是一个函子吗?看起来确实如此 - 它有一个映射函数,可以将集合转换为相同大小的集合并返回一个流。但另一方面,如前所述,这个映射函数实际上不会修改任何内容,直到我们调用某个终端函数。
在处理函子时,每当您看到模式时,您都可以依靠这些法则来帮助您重构和优化代码。
在这种情况下,当您使用
map(a).map(b)
时,您可以将其转换为 map(compose(b, a))
使用 compose 将运行
a
,首先在单个循环中将其返回值传递给 b
。使用地图map,您将循环数组两次。
const compose = (f, g) => x => f(g(x))
const add1 = x => x + 1
const add2 = x => x + 2
console.log(
[1,2].map(add1).map(add2)
)
console.log(
[1, 2].map(compose(add2, add1))
)
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js"></script>
链接映射函数时 - 它们是否在一个循环中执行?
[1,2].map(x => x + 1).map(x => x + 2); // [3,4]
答案是否定的,底层引擎不会做这种优化,代码实际上是按照你看到的方式执行的。与 Haskell 等其他语言不同的是,快捷融合等功能是其成功的关键部分。
但是,这并不意味着您无法在 javascript 中实现类似的优化级别。 :) 这就是为什么我想介绍
Transducer
的概念,我也建议你阅读这篇medium文章。
让我们回顾一下上面的例子, 你的算法的顺序是
O(2n)
,
这意味着我们对原始输入中的每个项目执行 2 次操作。
因此,在第一个地图和第二个地图之间会产生中间值,这在某种程度上也会严重影响性能。
为什么换能器可以派上用场?
const algorithm = R.compose(
R.map(R.inc),
R.map(R.multiply(2)),
);
const fn = R.into([], algorithm);
console.log(
fn([1, 2, 3, 4]),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous"></script>
从 ECMAScript 2025 开始,JavaScript 提供了 迭代器辅助方法,以便您获得所引用的行为(与 Java 流一样)。以下是它在您的示例中的运行情况:
const results = [1, 2].values() // Get iterator over the initial array
.map(x => x + 1) // A new iterator
.map(x => x + 2) // Yet another iterator
.toArray() // Consume iterators to create the result array
console.log(results); // [4, 5]
请注意,在您的问题中,您的代码注释有误。输出将是 [4, 5] 而不是 [3, 4],因为您最终将 3 添加到输入数组中给出的每个值。
在上面的代码中,
toArray
开始消耗前一个迭代器,进而从其前一个迭代器获取必要的值,...等。这样,值就会穿过“管道”,被映射,最终出现在最终的数组中。因此,该过程是“惰性的”,并且不会创建中间数组。