Array 实例方法 reduce 和 reduceRight 的实现

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 属性和整数键属性。

边界情况

  1. 如果数组为空,且未提供 initialValue,抛异常。
  2. 如果提供了 initialValue 且数组为空,返回 initialValue。
  3. 如果数组只有一个元素(无论位置如何)且未提供 initialValue,返回该元素。
  4. 如果数组有多个元素(无论位置如何)且未提供 initialValue,则 initialValue 初始化为数组中的第一个值,并且 callbackFn 从数组中的第二个值开始执行。
  5. 如果提供了 initialValue 且数组不为空,则 reduce 方法将始终从索引 0 开始调用回调函数。
  6. 返回累加完的初始值 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.definePropertyObject.defineProperties 方法可以特意控制的属性可能不可枚举。

4、处理边界情况

为了方便看我们上面写的边界情况,再复制过来一份。然后一个个处理边界情况。

  1. 如果数组为空,且未提供 initialValue,抛异常。
  2. 如果提供了 initialValue 且数组为空,返回 initialValue。
  3. 如果数组只有一个元素(无论位置如何)且未提供 initialValue,返回该元素。
  4. 如果数组有多个元素(无论位置如何)且未提供 initialValue,则 initialValue 初始化为数组中的第一个值,并且 callbackFn 从数组中的第二个值开始执行。
  5. 如果提供了 initialValue 且数组不为空,则 reduce 方法将始终从索引 0 开始调用回调函数。
  6. 返回累加完的初始值 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 实例方法,解析它们的实现原理。

如果有错误或者不严谨的地方,请请大家务必给予指正,十分感谢。欢迎大家在评论区中讨论。

相关推荐
子非鱼92129 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
想退休的搬砖人43 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript
清灵xmf1 小时前
揭开 Vue 3 中大量使用 ref 的隐藏危机
前端·javascript·vue.js·ref
蘑菇头爱平底锅1 小时前
十万条数据渲染到页面上如何优化
前端·javascript·面试
2301_801074152 小时前
TypeScript异常处理
前端·javascript·typescript