以前我没得选,现在只想用 Array.reduce

代码仓库:https://github.com/Haixiang6123/learn-reduce

前言

第一眼看 Array.reduce 这个函数总感觉怪怪的,用法也得花几分种才弄懂,懂了之后也不知道应用场景是啥。最近写项目的时候才慢慢对这个函数有更多的理解,可以算是 Array 类型下最强大的函数之一了。

API 用法

API 的用法分有无初始值两种情况:

没有初始值

const array = [1, 2, 3]

array.reduce((prev, curt) => {
  return prev + curt
}) // 1 + 2 + 3 = 6

有初始值

const array = [1, 2, 3]

array.reduce((prev, curt) => {
  return prev + curt
}, 99) // 99 + 1 + 2 + 3 = 105

reduce 这个函数的主要功能是将多个东西合成一个东西。大家都做过小学奥数吧,就类似于这样

reduce 所提供的功能就是这个加号,至于这怎么个加法,是由你来决定的。加法的过程可以形象地理解成贪吃蛇一样

🐍 (prev) + 💩(curt) + 💩 (curt) + 💩(curt) = 🛫 (return value)

已有的蛇身就是 prev 参数,要吃掉的豆子就是 curt,吃完豆子的状态就是回调函数的返回值,整个 reduce 函数返回值就是这条🐍死了之后的状态。

应用场景

reduce 这个函数最难的点是想不出有什么使用场景。下面就做个抛砖引玉:

那我们先来思考一个问题:上面的例子只展示了数字的加法嘛,而 JS 里有 7 种基本数据类型:number, string, object, symbol, null, boolean, undefined。如果这些类型相加会怎么样呢?

除了这些基本类型,object 里也有 Array,Function,Object,Promise 这些类型,将这些类型做加法是不是也可以作为很多应用场景呢?

还有另一个点就是,除了加法,我还可以做减法,甚至做 comparasion,max,min 等操作。

将上面这 3 点都用起来,不难发现单单一个 reduce 就可以有几十种玩法。下面就选几种比较典型的例子给大家一些灵感。

所有的代理(包括源码和测试代码)都放在这里:https://github.com/Haixiang6123/learn-reduce

max

Python 有这样的语法:max([1, 2, 3] // => 3,JS 是没有的,使用 reduce 就可以简单地实现上面的功能:

type TComparator<T> = (a: T, b: T) => boolean

function max<T>(array: T[], largerThan?: TComparator<T>): T {
  return array.reduce((prev, curt) => {
    return largerThan(prev, curt) ? prev : curt
  })
}

export default max

用例

describe('max', () => {
  it('返回简单元素最大值', () => {
    const array = [1, 2, 3, 4, 5]

    const result = max<number>(array, (a, b) => a > b)

    expect(result).toEqual(5)
  })
  it('返回复杂对象的最大值', () => {
    const array: TItem[] = [
      {value: 1}, {value: 2}, {value: 3}
    ]

    const result = max<TItem>(array, (a, b) => a.value > b.value)

    expect(result).toEqual({value: 3})
  })
})

findIndex

JS 有一个 Array.indexOf 的 API,但是对于像 [{id: 2}, {id: 3}] 这样的数据结构就不行了,我们一般希望传一个回调去找对应的对象的下标。使用 reduce 可以这么写:

type TEqualCallback<T> = (item: T) => boolean

function findIndex<T>(array: T[], isEqual: TEqualCallback<T>) {
  return array.reduce((prev: number, curt, index) => {
    if (prev === -1 && isEqual(curt)) {
      return index
    } else {
      return prev
    }
  }, -1)
}

export default findIndex

用例

describe('findIndex', () => {
  it('可以找到对应的下标', () => {
    const array: TItem[] = [
      {id: 1, value: 1},
      {id: 2, value: 2},
      {id: 3, value: 3},
    ]

    const result = findIndex<TItem>(array, (item) => item.id === 2)

    expect(result).toEqual(1)
  })
})

filter

使用 reduce 一样可以重新实现 Array 下的一些 API,比如 filter

type TOkCallback<T> = (item: T) => boolean

function filter<T>(array: T[], isOk: TOkCallback<T>): T[] {
  return array.reduce((prev: T[], curt: T) => {
    if (isOk(curt)) {
      prev.push(curt)
    }

    return prev
  }, [])
}

export default filter

用例

describe('filter', () => {
  it('可以过滤', () => {
    const array: TItem[] = [{id: 1}, {id: 2}, {id: 3}]

    const result = filter<TItem>(array, (item => item.id !== 1))

    expect(result).toEqual([{id: 2}, {id: 3}])
  })
})

normalize

在写 redux 的时候,我们有时可能会需要将数组进行 Normalization,比如

[{id: 1, value:1}, {id: 2, value: 2}]
=>
{
  1: {id: 1, value: 1}
  2: {id: 2, value: 2}
}

使用 reduce 可以先给个初始值 {} 来存放,然后每次只需要将 id => object 就可以了:

export type TUser = {
  id: number;
  name: string;
  age: number;
}

type TUserEntities = {[key: string]: TUser}

function normalize(array: TUser[]) {
  return array.reduce((prev: TUserEntities, curt) => {
    prev[curt.id] = curt

    return prev
  }, {})
}

export default normalize

用例

describe('normalize', () => {
  it('可以 normalize user list', () => {
    const users: TUser[] = [
      {id: 1, name: 'Jack', age: 11},
      {id: 2, name: 'Mary', age: 12},
      {id: 3, name: 'Nancy', age: 13}
    ]

    const result = normalize(users)

    expect(result).toEqual({
      1: users[0],
      2: users[1],
      3: users[2]
    })
  })
})

assign

上面例子也只是对 number 做相加,那对象“相加”呢?那就是 Object.assign 嘛,所以用 reduce 去做对象相加也很容易:

function assign<T>(origin: T, ...partial: Partial<T>[]): T {
  const combinedPartial = partial.reduce((prev, curt) => {
    return { ...prev, ...curt }
  })

  return { ...origin, ...combinedPartial }
}

export default assign

用例

describe('assign', () => {
  it('可以合并多个对象', () => {
    const origin: TItem = {
      id: 1,
      name: 'Jack',
      age: 12
    }

    const changeId = { id: 2 }
    const changeName = { name: 'Nancy' }
    const changeAge = { age: 13 }

    const result = assign<TItem>(origin, changeId, changeName, changeAge)

    expect(result).toEqual({
      id: 2,
      name: 'Nancy',
      age: 13
    })
  })
})

虽然这有点脱裤子放屁,但是如果将数组的里的每个对象都看成 [middleState, middleState, middleState, ...],初始值看成 prevState,最终生成结果看成成 nextState,是不是很眼熟?那不就是 redux 里的 reducer 嘛,其实也是 reducer 名字的由来。

concat

说了对象“相加”,数组“相加”也简单:

function concat<T> (arrayList: T[][]): T[] {
  return arrayList.reduce((prev, curt) => {
    return prev.concat(curt)
  }, [])
}

export default concat

用例

describe('concat', () => {
  it('可以连接多个数组', () => {
    const arrayList = [
      [1, 2],
      [3, 4],
      [5, 6]
    ]

    const result = concat<number>(arrayList)

    expect(result).toEqual([1, 2, 3, 4, 5, 6])
  })
})

functionChain

函数的相加不知道大家会想到什么,我是会想到链式操作,如 a().b().c()....,使用 reduce 实现,就需要传入这样的数组:[a, b, c]

function functionChain (fnList: Function[]) {
  return fnList.reduce((prev, curt) => {
    return curt(prev)
  }, null)
}

export default functionChain

用例

describe('functionChain', () => {
  it('可以链式调用数组里的函数', () => {
    const fnList = [
      () => 1,
      (prevResult) => prevResult + 2,
      (prevResult) => prevResult + 3
    ]

    const result = functionChain(fnList)

    expect(result).toEqual(6)
  })
})

promiseChain

既然说到了链式调用,就不得不说 Promise 了,数组元素都是 promise 也是可以进行链式操作的:

function promiseChain (asyncFnArray) {
  return asyncFnArray.reduce((prev, curt) => {
    return prev.then((result) => curt(result))
  }, Promise.resolve())
}

export default promiseChain

这里要注意初始值应该为一个 resolved 的 Promise 对象。

用例

describe('functionChain', () => {
  it('可以链式调用数组里的函数', () => {
    const fnList = [
      () => 1,
      (prevResult) => prevResult + 2,
      (prevResult) => prevResult + 3
    ]

    const result = functionChain(fnList)

    expect(result).toEqual(6)
  })
})

compose

最后再来说说 compose 函数,这是实现 middeware /洋葱圈模型最重要的组成部分.

为了造这种模型,我们不得不让函数疯狂套娃:

mid1(mid2(mid3()))

要构造上面的套娃,这样的函数一般叫做 compose,使用 reduce 实现如下:

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((aFunc, bFunc) => (...args) => aFunc(bFunc(...args)))
}

export default compose

用例

describe('compose', () => {
  beforeEach(() => {
    jest.spyOn(console, 'log')
  })

  afterEach(() => {
    clearAllMocks()
  })

  it('可以 compose 多个函数', () => {
    const aFunc = () => console.log('aFunc')
    const bFunc = () => console.log('bFunc')
    const cFunc = () => console.log('cFunc')

    compose(aFunc, bFunc, cFunc)()

    expect(console.log).toHaveBeenNthCalledWith(1, 'cFunc')
    expect(console.log).toHaveBeenNthCalledWith(2, 'bFunc')
    expect(console.log).toHaveBeenNthCalledWith(3, 'aFunc')
  })

  it('可以使用 next', () => {
    const aFunc = (next) => () => {
      console.log('before aFunc')
      next()
      console.log('after aFunc')

      return next
    }
    const bFunc = (next) => () => {
      console.log('before bFunc')
      next()
      console.log('after bFunc')

      return next
    }
    const cFunc = (next) => () => {
      console.log('before cFunc')
      next()
      console.log('after cFunc')

      return next
    }

    const next = () => console.log('next')

    const composedFunc = compose(aFunc, bFunc, cFunc)(next)
    composedFunc()

    expect(console.log).toHaveBeenNthCalledWith(1, 'before aFunc')
    expect(console.log).toHaveBeenNthCalledWith(2, 'before bFunc')
    expect(console.log).toHaveBeenNthCalledWith(3, 'before cFunc')
    expect(console.log).toHaveBeenNthCalledWith(4, 'next')
    expect(console.log).toHaveBeenNthCalledWith(5, 'after cFunc')
    expect(console.log).toHaveBeenNthCalledWith(6, 'after bFunc')
    expect(console.log).toHaveBeenNthCalledWith(7, 'after aFunc')
  })
})

总结

上面的例子仅仅给出了 reduce 实现的最常见的一些工具函数。

就像第2点说的不同类型和不同操作有非常多的组合方式,因此使用 reduce 可以玩出很多种玩法。希望上面给出的这些可以给读者带来一些思考和灵感,在自己项目里多使用 reduce 来减轻对数组的复杂操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342