JS精进系列之把reduce用起来

2,882 阅读5分钟

Array.prototype.reduce 是JS中一个很神奇的方法, 应该应用场景实在太多了, 比如 实现异步函数串行, 聚合数据等等。本文使用reduce来实现一下我们日常用使用到的工具函数, 希望每个例子都可以自己手动实现一遍, 习惯, 适应使用reduce来解决问题, 跟着JS标准更新自己的技术。

pipe 管道

pipe 管道可能是我们在linux下最常用的操作符之一了, 上一个函数的计算结果传给下一个函数, 就这样一直传递

应用举例: 当我们将函数拆分成更小粒度的时候, 我们可以用pipe将计算连接起来, 提高代码的可读性, 像下面这样的嵌套看着太累了, 写着也不舒服。


const calc1 = x => x * 2
const calc2 = x => x - 1
const calc3 = x => x * 3

const sum = calc3(calc2(calc1(10)))

使用pipe后

const calc = pipe(calc1, calc2, calc3)
const sum = calc(10)

使用 reduce 实现 pipe:

const pipe = (...functions) => (initialValue) =>
  functions.reduce((value, fn) => fn(value), initialValue);

reduce是个迭代器, 接受2个参数, 第二个参数可选, 是开始迭代的值,如果不填默认从数组第一个值开始, 第一个参数是一个回调函数, 函数中的第一个值是上次计算结果, 第二个值是当前遍历的值, 这个解释有点抽象, 自己跟着实现几个工具函数就理解了。

compose 组合

compose和pipe非常的像, pipe是从左到右执行顺序, compose是从右向左的执行顺序, 所以在实现上用了reduceRight这个方法

const compose = (...functions) => (initialValue) =>
  functions.reduceRight((value, fn) => fn(value), initialValue);

compact 去除假值

我们通常会使用filter来去除假值

const falseyArray = [0, null, 1, undefined, 2, '', 3, false, 4, NaN]

const compact = (arr) => arr.filter(el => el)
compact(falseyArray) // [1, 2, 3, 4]

其实使用reduce也是可以的

const compact = list => 
    list.reduce((acc, value) => {
        value && acc.push(value)
        return acc
    }, [])

实现includes

includes这个函数在简化条件判断时候也很有用

const value = 'cat'
if (value === 'cat' || value === 'dog' || value === 'pig'){
    // statement
}

// 可以该写为
if(['cat', 'dog', 'pig'].includes(value)){
    // statement
}

使用reduce实现

const includes = (item, list) =>
  list.reduce((isIncluded, value) => isIncluded || item === value, false);

flatten 扁平化数组

之前忘记从哪里看到过这么一道面试题, 是把嵌套数组扁平化, 也就是如何实现 Array.prototype.flatten

flatten([[1, 2], [3, 4]]);  // [1, 2, 3, 4]

实现:

function flatten(arr){
    return arr.reduce((pre, next) => {
        return pre.concat(Array.isArray(next) ? flatten(next) : next) 
    }, [])
}

除了reduce实现外还有几种其他实现:

  1. 使用toString实现
[[[1, 2], [1, 2, 3]], [1, 2]].toString().split(',').map(Number)
  1. 使用解构提取
const flatten = list => {
    while(list.some((item) => Array.isArray(item))){
        list = [].concat(...list)   
    }
    return list;
}

repeat

repeat是String具有的方法, 但在处理数据时候我们经常会使用复杂点的数据类型比如 object, 这时候repeat就不能使用了, 我们可以用reduce来实现一个

const repeat = (item, times) =>
  Array.from({ length: times }).reduce((acc) => {
    acc.push(item);

    return acc;
  }, []);
  
// 使用

repeat({ favoriteLanguage: 'JavaScript' }, 2);

/*
[{
    favoriteLanguage: 'JavaScript'
}, {
    favoriteLanguage: 'JavaScript'
}],
*/

times 多次执行

有时候想让一个计算多次执行并返回一个列表, 实现和上面的repeat很相似

times((x) => x * 2, 3);
// [0, 2, 4]

实现:

const times = (fn, numTimes) => Array.from({length: numTimes}).reduce((acc, _, index) => {
    acc.push(fn(index))
    return acc
}, [])

deduplicate 数组去重

面试题下次不让你用Set特性去重的时候, 你就可以写这个

const deduplicate = (items) => {
  const cache = {};

  return items.reduce((acc, item) => {
    const alreadyIncluded = cache[item] === true;

    if (!alreadyIncluded) {
      cache[item] = true;
      acc.push(item);
    }

    return acc;
  }, []);
};

deduplicate([[1], [1], { hello: 'world' }, { hello: 'world' }]);
// [[1], { hello: 'world' }]

reverse 逆序

reverse有个问题就是会改变原来的数组, 我们自己实现一个reverse来避免这种副作用

实现:

const reverse = list => list.reduceRight((acc, value) => {
    acc.push(value)
    return acc;
}, [])

reverse([1, 2, 3]);
// [3, 2, 1]

adjust

给数组中的指定位置的值应用某个函数, 比如 [1, -2, 3] 想给这个数组的第二个元素应用一个函数Math.abs()校正一下这个数据。如果给定的数组index越界的话就返回原数组。

const adjust = (fn, list, index) => {
    if(index > list.length) return list;
    
    list.reduce((acc, value, i) => {
        index === i ? acc.push(fn(value)) : acc.push(value)
        return acc;
    }, [])
}

fromPairs

函数作用:将一个pair数组转换成一个对象, 比如[['age', 20], ['name', 'evle']] 转换成 {name: 'evle', age: 10}

const fromPairs  = list => list.reduce((acc, pairs) => {
	const [k, v] = pairs;
	acc[k] = v
	return acc;
}, {})

range

js中没有range这种可以帮我们生成一个范围类型的工具函数比如: range(15, 20) // => 15, 16, 17, 18, 19, 20, 那我们自己实现一个

const range = (start, end) => Array.from({length: end - start + 1}).reduce((acc, _, index) => {
    acc.push(start + index)
    return acc;
}, [])

insertAll

在一个数组的指定位置插入一个数组, 如果给定的位置越界的话就插在最后面

insertAll(1, [2, 3, 4], [1, 5]);
// [1, 2, 3, 4, 5]

insertAll(10, [2, 3, 4], [1, 5]);
// [1, 5, 2, 3, 4]

实现:

const insertAll = (index, subList, list) => {
    if(list.length < index) {
        return list.concat(subList)
    }
    
    return list.reduce((acc, value, i) => {
        index === i ? acc.push(...subList, value) : acc.push(value)
        return acc;
    }, [])
}

mergeAll

把列表中的对象合并成一个对象

mergeAll([
    { js: 'reduce' },
    { elm: 'fold' },
    { java: 'collect' },
    { js: 'reduce' }
]);
  
/*
{
    js: 'reduce',
    elm: 'fold',
    java: 'collect'
}
*/

实现:

const mergeAll = (list) => list.reduce((acc, value) => {
    const [pair] = Object.entries(value)
    const [k, v] = pair;
    acc[k] = v;
    return acc
}, {})

xprod

xprod 是将给定数组中的元素组合成各种结果, 比如

xprod(['Hello', 'World'], ['JavaScript', 'Reduce']);
/*
[
  ['Hello', 'JavaScript'],
  ['Hello', 'Reduce'],
  ['World', 'JavaScript'],
  ['World', 'Reduce']
]
*/

实现:

const xprod = (list1, list2) => list1.reduce((acc, value) => {
    
    list2.forEach(el=>{
        acc.push([value, el])
    })
    
    return acc
}, []) 

intersperse

在数组的每个元素中间插入一个指定的元素

intersperse('Batman', [1, 2, 3, 4, 5, 6]);
// [1, 'Batman', 2, 'Batman', 3, 'Batman', 4, 'Batman', 5, 'Batman', 6]

intersperse('Batman', []);
// []

实现:

const intersperse = (spreator, list) => list.reduce((acc, value, index) => {
    index === list.length - 1 ? acc.push(value) : acc.push(value, spreator)
    return acc;
}, [])

insert

在数组的指定位置插入一个元素, 之前我们都是使用splice()来在数组中插入元素的, 下面我们用reduce实现一个

insert(2, ['Batman'], [1, 2, 3]);
// [1, 2, ['Batman'], 3]

insert(10, ['Batman'], [1, 2, 3]);
// [1, 2, 3, ['Batman']]

实现:

const insert = (index, newItem, list) => {
  if (index > list.length - 1) {
    return [...list, newItem];
  }

  return list.reduce((acc, value, sourceArrayIndex) => {
    index === sourceArrayIndex ? acc.push(newItem, value) : acc.push(value);
    return acc;
  }, []);
};

arrayIntoObject

有时候得到服务端的数据没有key, 数据没有任何规律, 为了方便操作我们一般会聚合数据

const users = [
    { username: 'JX01', status: 'offline' },
    { username: 'yazeedBee', status: 'online' }
];

// 实际上我们想要的是
{
  JX01: {
    username: 'JX01',
    status: 'offline'
  },
  yazeedBee: { username: 'yazeedBee', status: 'online' }
}
// 或者
{
  offline: {
    username: 'JX01',
    status: 'offline'
  },
  online: { username: 'yazeedBee', status: 'online' }
}

实现:

const arrayIntoObject = (key, list) =>
  list.reduce((acc, obj) => {
    const value = obj[key];

    acc[value] = obj;

    return acc;
  }, {});