教你玩转数组 reduce

reduce 是数组迭代器里的瑞士军刀。 它强大到您可以使用它去构建大多数其他数组迭代器方法,例如.map().filter().flatmap ()。在这篇文章中,我们将带你用它来做一些更有趣的事情。 阅读前,我们需要您对数组迭代器方法有一定的了解。

Reduce是迄今为止发现的最通用的功能之一
Eric Elliott

使用 reduce 做加法乘法还可以,可一旦要超出现有基础示例,人们就会觉着有些困难。更复杂的字符串什么的,可能就不行了。 使用 reduce 做和数字以外的事情,总会觉着有些怪怪的。

为什么 reduce() 会让人觉着很复杂?

我猜测主要有两个原因。 第一个是,我们更愿意教别人使用.map().filter() 却不教 reduce()reduce ()map() 或者.filter() 用起来的感觉非常不同。每个不一样的初始值,经过 reduce 之后,都会有不同的结果。类似将当前数组元素进行累加得到的值。

第二个原因是我们如何去教人们使用 .reduce()。下面这样的教程并不少见:

function add(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

const sampleArray = [1, 2, 3, 4];

const sum = sampleArray.reduce(add, 0);
console.log(‘The sum total is:, sum);
// ⦘ The sum total is: 10

const product = sampleArray.reduce(multiply, 1);
console.log(‘The product total is:, product);
// ⦘ The product total is: 24

我说这个不是针对个人, MDN 文档也是使用这样的例子。而且我自己也这样使用。 我们这样做是有原因的。 像 add ()multiply () 这样的函数很容易理解。 但有点太简单了。 对于 add () ,是 b + a 还是 a + b 并不重要,乘法也是一样。a * b 等于 b * a。 但实际上 reducer 函数中到底发生了什么。

Reducer 函数是给 .reduce() 传递的第一个参数 accumulator。 示意如下:

function myReducer(accumulator, arrayElement) {
    // Code to do something goes here
}

accumulator是一个叠加值。 它包含上次调用 reducer 函数时返回的所有内容。 如果 reducer 函数还没有被调用,那么它包含初始值。 因此,当我们传递 add () 作为 reducer 时,累加器映射到 a + ba 部分,而 a 恰好包含前面所有项目的运行总数。 对于 multiply ()也是一样。 a * b 中的 a 参数包含运行的乘法总数。 这些介绍没什么问题。 但是,它掩盖了一个 .reduce() 最有趣的特征。

reduce()有一个强大的能力是 accumulatorarrayElement 不必是相同的类型。 对于加法和乘法,是同一类型的,a 和 b 都是数字。 但其实我们不需要类型相同。 累加器可以是与数组元素完全不同的类型。

例如,我们的累加器可能是一个字符串,而我们的数组是数字:

function fizzBuzzReducer(acc, element) {
    if (element % 15 === 0) return `${acc}Fizz Buzz\n`;
    if (element % 5 === 0) return `${acc}Fizz\n`;
    if (element % 3 === 0) return `${acc}Buzz\n`;
    return `${acc}${element}\n`;
}

const nums = [
    1, 2, 3, 4, 5, 6, 7, 8, 9,
    10, 11, 12, 13, 14, 15
];

console.log(nums.reduce(fizzBuzzReducer, ''));

这个例子只是为了举例说明。 我们也可以使用 .map().join()来实现相同逻辑。 reduce() 不仅仅是对字符串好用。 accumulator 的值可以不是简单的类型(如数字或字符串)。还可以是一个结构化类型,比如数组或者普通的 ol'JavaScript 对象(POJO)。 接下来,我们做一些更有趣的事情。

我们可以用 reduce 做一些有趣的事情

那么,我们能做些什么有趣的事情呢? 我在这里列出了五个不同于数字相加的:

    1. 将数组转换为对象;
    1. 展开成一个更大的阵列;
    1. 在一个遍历中进行两次计算;
    1. 将映射和过滤合并为一个通道;
    1. 按顺序运行异步函数

将数组转换为对象

我们可以使用 .reduce() 将数组转换为 POJO。 如果您需要进行某种查找,这可能很方便。 例如,假如我们有一个人员列表:

const peopleArr  = [
    {
        username:    'glestrade',
        displayname: 'Inspector Lestrade',
        email:       'glestrade@met.police.uk',
        authHash:    'bdbf9920f42242defd9a7f76451f4f1d',
        lastSeen:    '2019-05-13T11:07:22+00:00',
    },
    {
        username:    'mholmes',
        displayname: 'Mycroft Holmes',
        email:       'mholmes@gov.uk',
        authHash:    'b4d04ad5c4c6483cfea030ff4e7c70bc',
        lastSeen:    '2019-05-10T11:21:36+00:00',
    },
    {
        username:    'iadler',
        displayname: 'Irene Adler',
        email:       null,
        authHash:    '319d55944f13760af0a07bf24bd1de28',
        lastSeen:    '2019-05-17T11:12:12+00:00',
    },
];

在某些情况下,通过用户名查找用户详细信息可能很方便。 为了方便起见,我们可以将数组转换为对象。 它可能看起来像这样:

function keyByUsernameReducer(acc, person) {
    return {...acc, [person.username]: person};
}
const peopleObj = peopleArr.reduce(keyByUsernameReducer, {});
console.log(peopleObj);
// ⦘ {
//     "glestrade": {
//         "username":    "glestrade",
//         "displayname": "Inspector Lestrade",
//         "email":       "glestrade@met.police.uk",
//         "authHash":    "bdbf9920f42242defd9a7f76451f4f1d",
//          "lastSeen":    "2019-05-13T11:07:22+00:00"
//     },
//     "mholmes": {
//         "username":    "mholmes",
//         "displayname": "Mycroft Holmes",
//         "email":       "mholmes@gov.uk",
//         "authHash":    "b4d04ad5c4c6483cfea030ff4e7c70bc",
//          "lastSeen":    "2019-05-10T11:21:36+00:00"
//     },
//     "iadler":{
//         "username":    "iadler",
//         "displayname": "Irene Adler",
//         "email":       null,
//         "authHash":    "319d55944f13760af0a07bf24bd1de28",
//          "lastSeen":    "2019-05-17T11:12:12+00:00"
//     }
// }

在这个版本中,对象中依然包含了用户名。 如果你不需要的话,可以移除。

将一个小阵列展开为一个大阵列

通常情况下,我们想到使用 .reduce() 就是将许多列表减少到一个值。 但是单一值也可以是个数组啊。 而且也没有规则说数组必须比原始数组短。 所以,我们可以使用 .reduce() 将短数组转换为长数组。

假设您从文本文件中读取数据。看下面这个例子。 我们在一个数组里放一些纯文本。 用逗号分隔每一行,而且假设是一个很大的名字列表。

const fileLines = [
    'Inspector Algar,Inspector Bardle,Mr. Barker,Inspector Barton',
    'Inspector Baynes,Inspector Bradstreet,Inspector Sam Brown',
    'Monsieur Dubugue,Birdy Edwards,Inspector Forbes,Inspector Forrester',
    'Inspector Gregory,Inspector Tobias Gregson,Inspector Hill',
    'Inspector Stanley Hopkins,Inspector Athelney Jones'
];

function splitLineReducer(acc, line) {
    return acc.concat(line.split(/,/g));
}
const investigators = fileLines.reduce(splitLineReducer, []);
console.log(investigators);
// ⦘ [
//   "Inspector Algar",
//   "Inspector Bardle",
//   "Mr. Barker",
//   "Inspector Barton",
//   "Inspector Baynes",
//   "Inspector Bradstreet",
//   "Inspector Sam Brown",
//   "Monsieur Dubugue",
//   "Birdy Edwards",
//   "Inspector Forbes",
//   "Inspector Forrester",
//   "Inspector Gregory",
//   "Inspector Tobias Gregson",
//   "Inspector Hill",
//   "Inspector Stanley Hopkins",
//   "Inspector Athelney Jones"
// ]

我们输入一个长度为5的数组,输出了一个长度为16的数组。

现在,你可能以前看过我的 JavaScript 数组方法文明指南。 那可能会记得我推荐使用 .flatMap() 来实现这个功能。 但是 .flatmap ()Internet ExplorerEdge 中是不可用的。 所以,我们可以使用 .reduce() 来自己实现一个 .flatMap () 函数。

function flatMap(f, arr) {
    const reducer = (acc, item) => acc.concat(f(item));
    return arr.reduce(reducer, []);
}

const investigators = flatMap(x => x.split(','), fileLines);
console.log(investigators);

reduce() 可以帮助我们把短数组变成长数组。而且它还可以覆盖那些不可用的丢失的数组方法。

在一个遍历中进行两次计算

有时我们需要一个数组进行两次计算。 假设,我们希望计算出一个数字列表里的最大值和最小值。 我们可能需要这样算两次:

const readings = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
const maxReading = readings.reduce((x, y) => Math.max(x, y), Number.MIN_VALUE);
const minReading = readings.reduce((x, y) => Math.min(x, y), Number.MAX_VALUE);
console.log({minReading, maxReading});
// ⦘ {minReading: 0.2, maxReading: 5.5}

遍历两次我们的数组。 但能不能一次解决呢?.reduce () 可以返回任何我们想要的类型,不必返回一个数字。 我们可以将两个值编码到一个对象中。 然后我们可以对每次迭代进行两次计算,只遍历一次数组:

const readings = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
function minMaxReducer(acc, reading) {
    return {
        minReading: Math.min(acc.minReading, reading),
        maxReading: Math.max(acc.maxReading, reading),
    };
}
const initMinMax = {
    minReading: Number.MAX_VALUE,
    maxReading: Number.MIN_VALUE,
};
const minMax = readings.reduce(minMaxReducer, initMinMax);
console.log(minMax);
// ⦘ {minReading: 0.2, maxReading: 5.5}

这个例子里,我们没有考虑到性能。我们仍然需要计算相同的数字。但是在某些情况下,可能会有本质区别。 比如,如果我们使用 .map().filter() 操作…

mapfilter传参

假设还是刚刚的那个 peopleArr 数组。 我们排除没有电子邮件地址的人,想找到最近登录的人。 一种方法是通过三个独立的操作:

    1. 过滤掉没有电子邮件的人;
    1. 找到最后登录时间
    1. 求最大值

按123写代码如下:

function notEmptyEmail(x) {
   return (x.email !== null) && (x.email !== undefined);
}

function getLastSeen(x) {
    return x.lastSeen;
}

function greater(a, b) {
    return (a > b) ? a : b;
}

const peopleWithEmail = peopleArr.filter(notEmptyEmail);
const lastSeenDates   = peopleWithEmail.map(getLastSeen);
const mostRecent      = lastSeenDates.reduce(greater, '');

console.log(mostRecent);
// ⦘ 2019-05-13T11:07:22+00:00

这段代码是易读且可执行的。 对于样本数据来说,这就足够了。 但如果我们有一个巨大的数组,那么我们可能会遇到内存问题。 因为我们使用了一个变量来存储每个中间数组。 那我们来修改一下我们的 reducer 方法,一次性完成所有的事情:

function notEmptyEmail(x) {
   return (x.email !== null) && (x.email !== undefined);
}

function greater(a, b) {
    return (a > b) ? a : b;
}
function notEmptyMostRecent(currentRecent, person) {
    return (notEmptyEmail(person))
        ? greater(currentRecent, person.lastSeen)
        : currentRecent;
}

const mostRecent = peopleArr.reduce(notEmptyMostRecent, '');

console.log(mostRecent);
// ⦘ 2019-05-13T11:07:22+00:00

在这个版本中,我们只需要遍历数组一次。 但是,如果人数很少的话,我依然会推荐您使用 .filter().map()。 如果您遇到来内存使用或性能问题,再考虑这样的替代方案。

按顺序执行异步函数

我们还可以使用 .reduce() 是实现按顺序执行(与并行相反)。如果对 API 请求有速率限制,或者需要将每个 promise 传递给下一个 promise,用这个方法会很方便。 举个例子,假设我们想要获取 peopleArr 数组中每个人的消息。

function fetchMessages(username) {
    return fetch(`https://example.com/api/messages/${username}`)
        .then(response => response.json());
}

function getUsername(person) {
    return person.username;
}

async function chainedFetchMessages(p, username) {
    // In this function, p is a promise. We wait for it to finish,
    // then run fetchMessages().
    const obj  = await p;
    const data = await fetchMessages(username);
    return { ...obj, [username]: data};
}

const msgObj = peopleArr
    .map(getUsername)
    .reduce(chainedFetchMessages, Promise.resolve({}))
    .then(console.log);
// ⦘ {glestrade: [ … ], mholmes: [ … ], iadler: [ … ]}

请注意,为了使代码正常工作,我们必须传入一个 Promise 作为使用 Promise.resolve () 的初始值。 resolve 将立即执行(Promise.resolve() 来实现)。 然后,我们第一次调用的API就会立即执行。

为什么我们很少会看到 reduce 的使用呢?

我已经为您展示了各式各样的使用 .reduce() 来实现的有趣的事。希望你可以在你的项目中真正的使用起来。 不过,.reduce() 如此强大和灵活,那么为什么我们很少看到它呢? 这是因为,.reduce() 可以做太多事情,导致很难具体描述。 反而是 .map(), .filter().flatMap() 缺少灵活性,我们会见到更多具体案例场景。还可以看到开发者的意图,让代码可读性更好,所以通常使用其他方法,比 reduce 的要多。

动手试试吧,我的朋友

现在你对 .reduce()有了改观性的认识,那要不要试试? 如果你在尝试过程中发现了我不知道的有趣的事,可以告诉 。我很乐意与你交流。

  1. 作者:@js 啦啦队长,2019年5月15日
  2. 如果你看一下 .reduce() 文档,您将看到 reducer 最多需要四个参数。 但是只有 accumulatorarrayElement 是必传。 为了简化,我这里没有传递完整参数。
  3. 一些读者可能会指出,我们可以通过改变 accumulator 来获得性能增益。 我们可以改变对象,而不是每次都使用 spread 操作符来创建一个新对象。 我这样编码是因为我想保持避免操作冲突。 但如果会影响性能,那我在实际生产环境代码中,可能会选择改变它。
  4. 如果您想知道如何并行运行 Promises,请查看 如何并行执行 Promise

原文链接: https://jrsinclair.com/articles/2019/functional-js-do-more-with-reduce/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值