以前我沒得選,現在只想用 Array.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。

應用場景

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 來減輕對數組的複雜操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章