因为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
}
复制代码
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
}, [])
}
复制代码
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
:
[...str].reduce()
Array.from({ length: num }).reduce()
Object.keys(obj).reduce()
Object.values(obj).reduce()
Object.entries(obj).reduce()
reduce的更多使用姿式期待你们一块儿发掘,欢迎在评论区留言讨论。