Array 实例方法 reduce 和 reduceRight 的实现
JavaScript 数组中的
reduce方法是一个强大的数组处理工具,它允许我们在迭代数组元素时,逐步累积结果并进行复杂的数据转换。本文将深入探讨reduce方法的实现,详细介绍 reduce 的各种细节,以帮助读者更好地理解和利用这个功能丰富的数组方法。无论您是初学者还是有经验的开发者,通过实现reduce方法,您将能够使用 reduce 编写更简洁、高效且可读性强的 JavaScript 代码。
Array.prototype.reduce()
reduce() 方法对数组中的每个元素按序执行一个提供的 reducer 函数。
每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
reduce 方法非常好用且功能强大,但是有点难理解。
语法
js
reduce(callbackFn)
reduce(callbackFn, initialValue)
参数
reduce 方法接受两个参数,第一个参数是回调函数,第二个参数是初始值。下面分别介绍这两个参数。
1、回调函数(callbackFn)是数组中每个元素执行的函数。返回值会作为下一次调用 callbackFn 时的累加器参数。 对于最后一次调用,返回值会作为 reduce() 的返回值。回调函数被调用时需要以下参数:
- 累加器(accumulator):上一次调用 callbackFn 的结果。在第一次调用时,如果指定了 reduce 第二个参数初始值,初始值作为累加器,否则拿数组第一个元素 array[0] 的值。
- 当前值(currentValue):当前元素的值。在第一次调用时,如果指定了reduce 第二个参数 initialValue,数组第一个元素 array[0] 作为当前值,否则数组第二个元素 array[1] 作为当前值。
- 当前索引(currentIndex):currentValue 在数组中的索引位置。在第一次调用时,如果指定了 初始值 initialValue 则为 0,否则为 1。
- 数组(array):调用了 reduce() 的数组本身。
2、初始值 initialValue(可选):初始值可以是任意值,第一次调用回调函数时初始化累加器 accumulator 的值。如果指定了 initialValue,则回调函数从数组中的第一个值作为当前值 currentValue 开始执行。
如果没有指定 initialValue,则累加器 accumulator 初始化为数组中的第一个值,并且回调函数从数组中的第二个值作为当前值 currentValue 开始执行。
在这种情况下,如果数组为空(没有第一个值可以作为累加器 accumulator 返回),则会抛出错误。

返回值
使用 "reducer" 回调函数遍历整个数组后的结果。
异常
TypeError:如果传的 callbackFn 不是函数,则抛出异常。
描述
reduce() 方法是一个迭代方法。它按升序对数组中的所有元素运行一个 "reducer" 回调函数,并将它们累积到一个单一的值中。
每次调用时,回调函数的返回值都作为累加器参数传递到下一次调用中。
累加器的最终值(也就是在数组的最后一次迭代中从回调函数返回的值)将作为 reduce() 的返回值。
回调函数 callbackFn 仅对已分配值的数组索引进行调用。不会对稀疏数组中的空槽进行调用。稀疏数组是指数组中大部分元素的值为空白(empty),比如下面这种数组。
js
Array(3) // [empty, empty, empty]
[, 1, 2, , 3,]
与其他迭代方法不同,reduce() 不接受 thisArg 参数,也就是 reduce 不需要 forEach、map 等方法一样传递 this 指向。
回调函数 callbackFn 调用时始终以 undefined 作为 this 的值。 如果 callbackFn 未处于严格模式,则该值将被替换为 globalThis。
reduce() 不会改变被调用的原数组,但是作为 callbackFn 提供的回调函数可能会改变原数组,比如回调函数中对原数组进行删除、排序等操作。
但需要注意的是,在第一次调用 callbackFn 之前,数组的长度会被保存。因此:
- 当开始调用 reduce() 时,callbackFn 将不会访问超出数组初始长度的任何元素。
- 对已访问索引的更改不会导致再次在这些元素上调用 callbackFn。
- 如果数组中一个现有的、尚未访问的元素被 callbackFn 更改,则它传递给 callbackFn 的值将是该元素被修改后的值。
被删除的元素则不会被访问。
上述类型的并发修改经常导致难以理解的代码,通常应避免(特殊情况除外)。
reduce() 方法是通用的。它只期望 this 值具有 length 属性和整数键属性。
边界情况
- 如果数组为空,且未提供 initialValue,抛异常。
- 如果提供了 initialValue 且数组为空,返回 initialValue。
- 如果数组只有一个元素(无论位置如何)且未提供 initialValue,返回该元素。
- 如果数组有多个元素(无论位置如何)且未提供 initialValue,则 initialValue 初始化为数组中的第一个值,并且 callbackFn 从数组中的第二个值开始执行。
- 如果提供了 initialValue 且数组不为空,则 reduce 方法将始终从索引 0 开始调用回调函数。
- 返回累加完的初始值 initialValue。
实现 reduce 方法
从上面 reduce 描述总结实现 reduce 时注意实现这两点。
- 对所有边界情况进行判断返回。
- 对稀疏数组进行处理。
1、原型上挂载 reduce 函数
数组的实例方法都继承于构造函数(Array)的原型(prototype),为了区分原生 reduce,我们取名为 myReduce。
reduce 方法接受两个参数:回调函数 callbackFn(必需) 和 初始值 initialValue(可选)。
js
Array.prototype.myReduce = function (callbackFn, initialValue) {
}
2、参数校验
- 回调函数 callbackFn 是必需,所以我们需要校验。如果 callbackFn 不是函数我们抛出异常。
js
Array.prototype.myReduceRight = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
}
Object.is是在 ECMAScript 2015(ES6)中引入。相比于传统的==和===运算符,Object.is处理了一些特殊情况,使得在一些边缘情况下比较更为准确。比如:+0 和 -0,NaN 等。
3、定义需要的变量
- noInitialValue:判断第二个参数 初始值 initialValue 是否有值。用 undefined 判断初始值是否传值会不严谨,会有初始值就是传 undefined 的情况。所以我们需要用 arguments 来判断是否传值。
- arrKeys:获取当前数组实例仅对已分配值的数组索引数组。稀疏数组中的空槽位置索引不会获取。
- arrLength:获取当前数组实例已分配值的索引数组长度。
这三个变量在处理边界情况时用到。
js
Array.prototype.myReduceRight = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
}
arguments:Arguments 对象也称之为实参对象。Arguments 对象只定义在函数体中,只在函数体中访问。包括了函数的实体参数和其他属性。在函数体中,arguments指代该函数的 Arguments 对象。
Object.keys()ECMAScript 2009(ES5)中引入。是一个静态方法,用于返回一个给定对象自身可枚举属性的数组。可枚举属性是指可以被遍历的属性(例如
for...in循环),大多数对象属性都可枚举。 Symbol 或者通过Object.defineProperty和Object.defineProperties方法可以特意控制的属性可能不可枚举。
4、处理边界情况
为了方便看我们上面写的边界情况,再复制过来一份。然后一个个处理边界情况。
- 如果数组为空,且未提供 initialValue,抛异常。
- 如果提供了 initialValue 且数组为空,返回 initialValue。
- 如果数组只有一个元素(无论位置如何)且未提供 initialValue,返回该元素。
- 如果数组有多个元素(无论位置如何)且未提供 initialValue,则 initialValue 初始化为数组中的第一个值,并且 callbackFn 从数组中的第二个值开始执行。
- 如果提供了 initialValue 且数组不为空,则 reduce 方法将始终从索引 0 开始调用回调函数。
- 返回累加完的初始值 initialValue。
1、如果数组为空,且未提供 initialValue,抛异常
js
Array.prototype.myReduce = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
if (noInitialValue && !arrLength) {
throw TypeError('Reduce of empty array with no initial value')
}
}
2、如果提供了 initialValue 且数组为空,返回 initialValue
js
Array.prototype.myReduce = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
if (noInitialValue && !arrLength) {
throw TypeError('Reduce of empty array with no initial value')
}
if (!noInitialValue && !arrLength) {
return initialValue
}
}
3、如果数组只有一个元素(无论位置如何)且未提供 initialValue,返回该元素
js
Array.prototype.myReduce = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
if (noInitialValue && !arrLength) {
throw TypeError('Reduce of empty array with no initial value')
}
if (!noInitialValue && !arrLength) {
return initialValue
}
if (noInitialValue && arrLength === 1) {
return this[arrKeys[0]]
}
}
4、如果数组有多个元素(无论位置如何)且未提供 initialValue,则 initialValue 初始化为数组中的第一个值,并且 callbackFn 从数组中的第二个值开始执行
- initIndex:为了确定回调函数 callbackFn 从第几个索引开始执行。
js
Array.prototype.myReduce = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
if (noInitialValue && !arrLength) {
throw TypeError('Reduce of empty array with no initial value')
}
if (!noInitialValue && !arrLength) {
return initialValue
}
if (noInitialValue && arrLength === 1) {
return this[arrKeys[0]]
}
let initIndex = 0
if (noInitialValue && arrLength > 1) {
initialValue = this[arrKeys[0]]
initIndex = +arrKeys[1]
}
}
+arrKeys[1]是因为Object.keys()返回值数组里的元素都是字符串,所以用 + 号转成数字,也可以用Number(arrKeys[1])。
5、如果提供了 initialValue 且数组不为空,则 reduce 方法将始终从索引 0 开始调用回调函数
上面描述中写到:
-
callbackFn 仅对已分配值的数组索引进行调用。不会对稀疏数组中的空槽进行调用。 上面介绍 callbackFn 时写到会接受四个参数:累加器(accumulator)、当前值(currentValue)、当前索引(currentIndex)、数组(array)。调用时把这四个参数依次传进去即可。
-
reduce() 不接受 thisArg 参数,所以直接调用 callbackFn 即可,不用写 callbackFn.call()、callbackFn.apply() 等。
js
Array.prototype.myReduce = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
if (noInitialValue && !arrLength) {
throw TypeError('Reduce of empty array with no initial value')
}
if (!noInitialValue && !arrLength) {
return initialValue
}
if (noInitialValue && arrLength === 1) {
return this[arrKeys[0]]
}
let initIndex = 0
if (noInitialValue && arrLength > 1) {
initialValue = this[arrKeys[0]]
initIndex = +arrKeys[1]
}
for (let i = initIndex, length = this.length; i < length; i++) {
if (Object.hasOwn(this, i)) {
initialValue = callbackFn(initialValue, this[i], i, this)
}
}
}
Object.hasOwn()ECMAScript 2022(ES13)中引入,是一个静态方法。如果指定的对象自身有指定的属性,则Object.hasOwn()返回true。如果属性是继承的或者不存在,该方法返回false。这里
Object.hasOwn()判断是为了跳过稀疏数组中的稀疏元素。
6、返回累加完的初始值 initialValue
js
Array.prototype.myReduce = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
if (noInitialValue && !arrLength) {
throw TypeError('Reduce of empty array with no initial value')
}
if (!noInitialValue && !arrLength) {
return initialValue
}
if (noInitialValue && arrLength === 1) {
return this[arrKeys[0]]
}
let initIndex = 0
if (noInitialValue && arrLength > 1) {
initialValue = this[arrKeys[0]]
initIndex = +arrKeys[1]
}
for (let i = initIndex, length = this.length; i < length; i++) {
if (Object.hasOwn(this, i)) {
initialValue = callbackFn(initialValue, this[i], i, this)
}
}
return initialValue
}
最终完整实现代码
js
Array.prototype.myReduce = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
if (noInitialValue && !arrLength) {
throw TypeError('Reduce of empty array with no initial value')
}
if (!noInitialValue && !arrLength) {
return initialValue
}
let initIndex = 0
if (noInitialValue && arrLength > 0) {
if (arrLength === 1) {
return this[arrKeys[0]]
}
initialValue = this[arrKeys[0]]
initIndex = +arrKeys[1]
}
for (let i = initIndex, length = this.length; i < length; i++) {
if (Object.hasOwn(this, i)) {
initialValue = callbackFn(initialValue, this[i], i, this)
}
}
return initialValue
}
实现 reduceRight 方法
reduceRight() 方法对累加器(accumulator)和数组的每个值(按从右到左的顺序)应用一个函数,并使其成为单个值。
所有的行为跟 reduce 反着来就行。
js
Array.prototype.myReduceRight = function (callbackFn, initialValue) {
if (!Object.is(typeof callbackFn, 'function')) {
throw TypeError(`${typeof callbackFn} is not a function`)
}
const noInitialValue = Object.is(arguments.length, 1)
const arrKeys = Object.keys(this)
const arrLength = arrKeys.length
if (noInitialValue && !arrLength) {
throw TypeError('Reduce of empty array with no initial value')
}
if (!noInitialValue && !arrLength) {
return initialValue
}
if (noInitialValue && arrLength === 1) {
return this[arrKeys.at(-1)]
}
let initIndex = this.length - 1
if (noInitialValue && arrLength > 1) {
initialValue = this[arrKeys.at(-1)]
initIndex = +arrKeys.at(-2)
}
for (let i = initIndex; i >= 0; i--) {
if (Object.hasOwn(this, i)) {
initialValue = callbackFn(initialValue, this[i], i, this)
}
}
return initialValue
}
Array.prototype.at()在 ECMAScript 2022(ES13)中引入,是一个数组实例方法。该方法接收一个整数值并返回该索引对应的元素,允许正数和负数。负整数从数组中的最后一个元素开始倒数。
结语
到这里 Array 实例方法 reduce 和 reduceRight 的实现完成啦。
大家可以用一些测试用例跟原生 reduce 和 reduceRight 比较我们自己实现的 myReduce 和 myReduceRight 看看一样不。
Array 实例方法实现系列
JavaScript 中的 Array 类型提供了一系列强大的实例方法。在这个专栏中,我将深入探讨一些常见的 Array 实例方法,解析它们的实现原理。
如果有错误或者不严谨的地方,请请大家务必给予指正,十分感谢。欢迎大家在评论区中讨论。