学习折叠 JS 数组 什么是折叠? 创建折叠

2025-06-09

学习折叠 JS 数组

什么是折叠?

创建折叠

你可能遇到过需要获取一个数组并“收集”它们的情况。我的意思是,对数组执行一些操作,以便最终只获取单个值。以下是一些示例。

您之前肯定必须对数字数组进行求和:

function sum(numbers) {
    let acc = 0;
    for (const num of numbers) {
        acc = add(acc, num);
    }
    return acc;
}
Enter fullscreen mode Exit fullscreen mode

或者获取数字数组的乘积:

function prod(numbers) {
    let acc = 1;
    for (const num of numbers) {
        acc = mult(acc, num);
    }
    return acc;
}
Enter fullscreen mode Exit fullscreen mode

或者在数字数组中找到最大的数字:

function maximum(numbers) {
    let acc = -Infinity;
    for (const num of numbers) {
        acc = max(acc, num);
    }
    return acc;
}
Enter fullscreen mode Exit fullscreen mode

在每个例子中,我们都获取了一系列事物并执行了一些操作,将这些事物收集成一个事物。

什么是折叠?

上述示例有一些共同点。它们都涉及一些非常相似的部分:

  • 保存最终结果的地方,通常称为累积或acc
  • 累积的初始值(0、1 和-Infinity
  • 将累加和我们当前正在处理的数组项结合起来的二元运算(addmultmax

这个收集物品的过程显然遵循一种模式。我们目前重复了很多代码,所以如果我们能将它们抽象成一个函数,我们的代码就会更加简洁,表达能力也更强。这种函数有个名字叫 Fold(维基百科)。这个函数是函数式编程的基础之一。我们要做的是自己用 JS 实现这个 Fold,何乐而不为呢?

一些观察

关于折叠,有三件事值得注意。

二元运算addmultmax被称为reducers。 Reducer 接受两个值——当前累加值和当前数组元素——并返回新的累加值。

初始值需要是identity相对于 Reducer 的 。这意味着当初始值与另一个值 一起传递给 Reducer 时x,输出始终为x。示例:
add(0, x) = x
mult(1, x) = x
max(-Infinity, x) = x
这里,01-Infinity分别相对于 Reducer addmultmax是恒等式。我们需要它是 ,identity因为我们希望初始累加为“空”。0相对于求和为空,1相对于乘积为空。

所有数组元素必须属于同一数据类型(例如 type A),但累加元素的数据类型(例如B)不必与数组元素的数据类型相同。例如,此代码将一个数字数组折叠成一个字符串。

// nothing const concatNum = (x, y) => x + y.toString(); // concatenates a string x and number y const numbers = [1, 2, 3, 4, 5]; // elements are of type number let acc = ''; // accumulation is of type string for (const num of numbers) { acc = concatNum(acc, num); } console.log(acc);

注意减速器的接口必须是reducer(acc: B, x: A): B,在本例中是

concatNum(acc: string, x: number): string
Enter fullscreen mode Exit fullscreen mode

创建折叠

说了这么多,我们终于可以开始写 fold 函数了。fold 是一个高阶函数(我强烈推荐用Eloquent JavaScript来介绍 HOF),它接受一个 Reducer(一个函数)、一个用于累加的初始值和一个数组(更正式的说法是列表,也就是JS 数组)。

我们首先泛化 add/mult/max 的 reducer,并命名为reducer(惊喜!)。我们将初始值称为init。然后我们泛化数组。它可以是任何内容的数组,而不仅仅是数字,所以我们将其命名为xs。现在,我们定义了 fold!

const fold = (reducer, init, xs) => {
    let acc = init;
    for (const x of xs) {
        acc = reducer(acc, x);
    }
    return acc;
};
Enter fullscreen mode Exit fullscreen mode

你注意到参数传入折叠的顺序了吗?我们先传入reducer,然后传入init,最后传入是有原因的。这和柯里化xs有关,我们以后会再讲。上面的例子现在看起来像这样,粗箭头样式:

const sum = xs => fold(add, 0, xs);
const prod = xs => fold(mult, 1, xs);
const maximum = xs => fold(max, -Infinity, xs);
Enter fullscreen mode Exit fullscreen mode

好多了。

如果需要,我们可以内联编写 reducer:

const sum = xs => fold((acc, x) => acc + x, 0, xs);
const prod = xs => fold((acc, x) => acc * x, 1, xs);
const maximum = xs => fold((acc, x) => (acc >= x) ? acc : x, -Infinity, xs);
Enter fullscreen mode Exit fullscreen mode

这是一个供您使用的交互式编辑器:

// nothing const fold = (reducer, init, xs) => { let acc = init; for (const x of xs) { acc = reducer(acc, x); } return acc; }; const sum = xs => fold((acc, x) => acc + x, 0, xs); const prod = xs => fold((acc, x) => acc * x, 1, xs); const maximum = xs => fold((acc, x) => (acc >= x) ? acc : x, -Infinity, xs); const numbers = [3, 7, 1, 2, 5]; console.log('sum:', sum(numbers)); console.log('product:', prod(numbers)); console.log('maximum:', maximum(numbers));

很简单,对吧?好吧,我们有点作弊了。我们在 fold 定义中使用了 for 循环(更确切地说是 for...of 循环),这在函数式编程的世界里是大忌。使用 for 循环进行数据转换意味着我们必须修改一些对象。在这里,我们acc通过在循环中重新赋值来实现修改。真正的 fold 函数式实现应该使用递归,这样可以避免修改。我们将在另一篇文章中探讨这个问题。

给感兴趣的人一些提示

  • JS 已经有一个 fold 方法,它可用于数组。它叫做reduce。所以你可能会说我们自己重新实现 fold 是毫无意义的 🤷‍♂️(尽管我希望它能对一些 FP 新手有所帮助)。
  • 因为我们使用了 for...of 循环而不是普通的 for 循环,所以我们所做的折叠不仅适用于数组,还适用于任何可迭代对象
  • 一般来说,折叠应该适用于任何可枚举数据源,例如列表和树。
  • “收集”的概念不一定非要像加法或乘法那样组合数组元素。它可以是“查找和替换”,例如最大/最小 Reducer,也可以是“顺序应用”,例如将函数应用 Reducer 连接到管道函数(如果你感兴趣的话)。应用无穷无尽!

一个函数接受一堆参数却只返回一个值,这看起来可能有点琐碎,但我们会在下一篇文章中通过实现多个折叠来见证它到底有多强大。我们将扁平化数组、管道函数,并且(希望)用折叠实现更多功能。

鏂囩珷鏉ユ簮锛�https://dev.to/mebble/learn-to-fold-your-js-arrays-2o8p
PREV
阵列折叠能做什么?
NEXT
使用 Heroku 部署 React-Django 应用 React-Django-Heroku Web 应用部署方法 1:分离后端和前端引用