完全搞懂數組reduce方法

因爲reduce是一個相對較難的知識點且我認爲比較重要,因此就單獨發一篇,JS系列文章可前往小弟博客交流,後續的整理也會在博客及時更新,博客地址github.com/logan70/Blo…html

語法

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])git

參數

  • callback: 執行數組中每一個值 (若是沒有提供 initialValue則第一個值除外)的函數,包含四個參數:
    • accumulator: 累計器累計回調的返回值; 它是上一次調用回調時返回的累積值,或initialValue(見於下方)。
    • currentValue: 數組中正在處理的元素。
    • index(可選): 數組中正在處理的當前元素的索引。 若是提供了initialValue,則起始索引號爲0,不然從索引1起始。
    • array(可選): 調用reduce()的數組
  • initialValue(可選): 做爲第一次調用 callback函數時的第一個參數的值。 若是沒有提供初始值,則將使用數組中的第一個元素。 在沒有初始值的空數組上調用 reduce 將報錯。

基礎使用

單看概念有點繞,reduce到底有什麼用呢?只要記住reduce方法的核心做用就是聚合便可。github

何謂聚合操做一個已知數組來獲取一個任意類型的值就叫作聚合,這種狀況下用reduce準沒錯,下面來看幾個實際應用:數組

聚合爲數字:數組元素求和、求積、求平均數等

// 求總分
const sum = arr => arr.reduce((total, { score }) => total + score, 0)
// 求平均分
const average = arr => arr.reduce((total, { score }, i, array) => {
  // 第n項以前均求和、第n項求和後除以數組長度得出平均分
  const isLastElement = i === array.length - 1
  return isLastElement
    ? (total + score) / array.length 
    : total + score
}, 0)
const arr = [
  { name: 'Logan', score: 89 },
  { name: 'Emma', score: 93 },
]
expect(sum(arr)).toBe(182)
expect(average(arr)).toBe(91)
複製代碼

用僞代碼解析求總分執行順序以下:函數

const arr = [
  { name: 'Logan', score: 89 },
  { name: 'Emma', score: 93 },
]
const initialValue = 0
let total = initialValue
for (let i = 0; i < arr.length; i++) {
  const { score } = arr[i]
  total += score
}
expect(total).toBe(182)
複製代碼

經過上方例子你們應該基本瞭解了reduce的執行機制,下面就來看下其餘實際應用場景。post

聚合爲字符串

const getIntro = arr => arr.reduce((str, {
  name,
  score,
}) => `${str}${name}'s score is ${score};`, '')

const arr = [
  { name: 'Logan', score: 89 },
  { name: 'Emma', score: 93 },
]
expect(getIntro(arr))
  .toBe('Logan\'s score is 89;Emma\'s score is 93;')
複製代碼

聚合爲對象

下方代碼生成一個key爲分數,value爲對應分數的姓名數組的對象。測試

const scoreToNameList = arr => arr.reduce((map, { name, score }) => {
  (map[score] || (map[score] = [])).push(name)
  return map
}, {})

const arr = [
  { name: 'Logan', score: 89 },
  { name: 'Emma', score: 93 },
  { name: 'Jason', score: 89 },
]
expect(scoreToNameList(arr)).toEqual({
  89: ['Logan', 'Jason'],
  93: ['Emma'],
})
複製代碼

深刻理解

何爲未傳入初始值

大部分實現reduce的文章中,經過檢測第二個參數是否爲undefined來斷定是否傳入初始值,這是錯誤的。ui

未傳入就是嚴格的未傳入,只要傳入了值,哪怕是undefined或者null,也會將其做爲初始值。this

const arr = [1]

// 未傳入初始值,將數組第一項做爲初始值
expect(arr.reduce(initialVal => initialVal)).toBe(1)
// 傳入 undefined 做爲初始值,將 undefined 做爲初始值
expect(arr.reduce(initialVal => initialVal, undefined)).toBeUndefined()
// 傳入 null 做爲初始值,將 null 做爲初始值
expect(arr.reduce(initialVal => initialVal, null)).toBeNull()
複製代碼

因此本身實現reduce時能夠經過arguments.length判斷是否傳入了第二個參數,來正確斷定是否傳入了初始值。spa

未傳入初始值時的初始值

大部分實現reduce的文章中,reduce方法未傳入初始值時,直接使用數組的第一項做爲初始值,這也是錯誤的。

未傳入初始值時應使用數組的第一個不爲空的項做爲初始值

不爲空是什麼意思呢,就是並未顯式賦值過的數組項,該項不包含任何實際的元素,不是undefined,也不是null,在控制檯中打印表現爲empty,我目前想到的有三種狀況:

  • new Array(n),數組內均爲空項;
  • 字面量數組中逗號間不賦值產生空項;
  • 顯式賦值數組length屬性至數組長度增長,增長項均爲空項。

測試代碼以下:

const arr1 = new Array(3)
console.log(arr1) // [empty × 3]
arr1[2] = 0
console.log(arr1) // [empty × 2, 0]
// 第1、第二項爲空項,取第三項做爲初始值
expect(arr1.reduce(initialVal => initialVal)).toBe(0)

const arr2 = [ , , true]
console.log(arr2) // [empty × 2, true]
// 第1、第二項爲空項,取第三項做爲初始值
expect(arr2.reduce(initialVal => initialVal)).toBe(true)

const arr3 = []
arr3.length = 3 // 修改length屬性,產生空
console.log(arr3) // [empty × 3]
arr3[2] = 'string'
console.log(arr3) // [empty × 2, "string"]
// 第1、第二項爲空項,取第三項做爲初始值
expect(arr3.reduce(initialVal => initialVal)).toBe('string')
複製代碼

空項跳過迭代

大部分實現reduce的文章中,都是直接用for循環一把梭,將數組每項代入callback中執行,這仍是錯的。

錯誤一

上面說了未傳入初始值時使用數組的第一個非空項做爲初始值,這種狀況下,第一個非空項及其以前的空項均不參與迭代

const arr = new Array(4)
arr[2] = 'Logan'
arr[3] = 'Emma'

let count = 0 // 記錄傳入reduce的回調的執行次數
const initialVal = arr.reduce((initialValue, cur) => {
  count++
  return initialValue
})
// 未傳入初始值,跳過數組空項,取數組第一個非空項做爲初始值
expect(initialVal).toBe('Logan')
// 被跳過的空項及第一個非空項均不參與迭代,只有第四項'Emma'進行迭代,故count爲1
expect(count).toBe(1)
複製代碼

錯誤二

迭代過程當中,空項也會跳過迭代

const arr = [1, 2, 3]
arr.length = 10
console.log(arr) // [1, 2, 3, empty × 7]

let count = 0 // 記錄傳入reduce的回調的執行次數
arr.reduce((acc, cur) => {
  count++
  return acc + cur
}, 0)

// arr中第三項以後項均爲空項,跳過迭代,故count爲3
expect(count).toBe(3)
複製代碼

本身實現reduce時,能夠經過i in array判斷數組第i項是否爲空項。

實現reduce

說完了一些坑點,下面就來實現一個reduce

Array.prototype._reduce = function(callback) {
  // 省略參數校驗,如this是不是數組等
  const len = this.length
  let i = 0
  let accumulator

  // 傳入初始值則使用
  if (arguments.length >= 2) {
    accumulator = arguments[1]
  } else {
    // 未傳入初始值則從數組中獲取
    // 尋找數組中第一個非空項
    while (i < len && !(i in this)) {
      i++
    }
    
    // 未傳入初始值,且數組無非空項,報錯
    if (i >= len) {
      throw new TypeError( 'Reduce of empty array with no initial value' )
    }
    // 此處 i++ ,先返回i,即將數組第一個非空項做爲初始值
    // 再+1,即數組第一個非空項跳過迭代
    accumulator = this[i++]
  }

  while (i < len) {
    // 數組中空項不參與迭代
    if (i in this) {
      accumulator = callback(accumulator, this[i], i, this)
    }

    i++
  }
  return accumulator
}
複製代碼

reduce拓展用法

扁平化數組

Array.prototype._flat = function(depth = 1) {
  const flatBase = (arr, curDepth = 1) => {
    return arr.reduce((acc, cur) => {
      // 當前項爲數組,且當前扁平化深度小於指定扁平化深度時,遞歸扁平化
      if (Array.isArray(cur) && curDepth < depth) {
        return acc.concat(flatBase(cur, ++curDepth))
      }
      return acc.concat(cur)
    }, [])
  }
  return flatBase(this)
}
複製代碼

合併屢次數組迭代操做

相信你們平時會遇到屢次迭代操做一個數組,好比將一個數組內的值求平方,而後篩選出大於10的值,能夠這樣寫:

function test(arr) {
  return arr
    .map(x => x ** 2)
    .filter(x => x > 10)
}
複製代碼

只要是多個數組迭代操做的狀況,均可以使用reduce代替:

function test(arr) {
  return arr.reduce((arr, cur) => {
    const square = cur ** 2
    return square > 10 ? [...arr, square] : arr
  }, [])
}
複製代碼

串行執行Promises

function runPromisesSerially(tasks) {
  return tasks.reduce((p, cur) => p.then(cur), Promise.resolve())
}
複製代碼

執行函數組合

function compose(...fns) {
  // 初始值args => args,兼容未傳入參數的狀況
  return fns.reduce((a, b) => (...args) => a(b(...args)), args => args)
}
複製代碼

萬物皆可reduce

reduce方法是真的越用越香,其餘類型的值也可轉爲數組後使用reduce

  • 字符串[...str].reduce()
  • 數字Array.from({ length: num }).reduce()
  • 對象
    • Object.keys(obj).reduce()
    • Object.values(obj).reduce()
    • Object.entries(obj).reduce()

reduce的更多使用姿式期待你們一塊兒發掘,歡迎在評論區留言討論。

參考文章

相關文章
相關標籤/搜索