再学一学“reduce”

410 阅读9分钟

写在开头

文章有点长,但不粗,忍一忍~

  • 引言中的案例基本可以应对常见的所有场景
  • 第二小节开始会详细介绍 reduce 的语法

1. 引言

  • reduce 是 JavaScript 数组的一种高阶函数,主要用于将数组中的所有元素通过指定的函数合并为单一值。它可以用于求和、拼接、统计等多种场景。

    // 列举一些常见的场景
    
    /*
    数组求和:将数组中的所有数字进行相加
    */
    const numbers = [1, 2, 3, 4];
    const total = numbers.reduce((count, current) => count + current, 0);
    
    /*
    计算数组中的最大值或最小值:找到数组中的最大值或最小值
    */
    const numbers = [1, 2, 3, 4];
    const max = numbers.reduce((result, current) => Math.max(acc, current), numbers[0])
    /*
    稍加讲解
    - reduce 本身用于将数组中的所有元素通过一个累加器(result)合并成一个单一的值
    - “初始值 numbers[0]”。reduce方法的第二个参数是初始值,在这里设置的为 numbers 数组的第一个元素。这意味着第一次调用回调函数时,acc 的值是 numbers[0],而 current 的值是数组的 第二个元素(numbers[1])
    */
    
    /*
    统计元素频率:统计数组中每个元素出现的次数。
    */
    const strings = ['apple', 'banana', 'apple', 'origin', 'banana'];
    const totals = strings.reduce((total, string) => {
        total[string] = (total[string] || 0) + 1;
        return total
    }, {})
    
    /*
    数组扁平化:将嵌套的数组扁平化为单一数组
    */
    const nestedArr = [[1, 2], [3, 4], [5]]
    const flatArr = nestedArr.reduce((flat, current) => flat.concat(current), [])
    
    /*
    数组转对象:将数组转换为对象,例如将标准键值对数组转为对象
    */
    const mapsObj = [['a': 1], ['b', 2]];
    const reduceObj = mapsObj.reduce((count, [key, value]) => ({...count, [key]: value}), {})
    
    /*
    合并对象:合并多个对象为一个对象
    */
    const objects = [{a: 1}, {b: 2}, {c: 3}];
    const merged = objects.reduce((count, obj) => ({...count, ...obj}), {})
    
    /*
    生成累计和数组:生成一个新的数组,其中每个元素是源数组的累计和。
    */
    const numbers = [1, 2, 3, 4, 5];
    const addSum = numbers.reduce((count, current, index) => {
        if(index === 0) return [current];
        count.push(count[index - 1] + current);
        return count;
    }, [])
    /*
    稍加讲解
    - if(index === 0) return [current] 如果是第一个元素,就直接返回,也可理解为累加的初始条件
    - count[index - 1] + current 正常累加每一项的值,进行推数组
    - return count  返回每次更新后的 count 数组,以便下一次迭代继续使用
    */
    
  • reduce 方法的灵活性和强大功能使其成为处理数组数据时非常实用的工具,能够提高代码的简洁性和可读性。

2. reduce 的基本概念

  • 定义和语法array.reduce(callback(prev, curr, index, arr)[, initialValue]),其中 callback 是执行计算的函数,initialValue 是可选的初始值。
  • 参数解释
    • (prev)累加器:上一次调用 callback 返回的累积值,或初始值。
    • (curr)当前值:数组中正在处理的元素。
    • (index)当前索引:当前值在数组中的索引。
    • (arr)源数组:调用 reduce 的数组。

3. reduce 的基本用法

  • 示例:简单求和

    const numbers = [1, 2, 3, 4];
    const sum = numbers.reduce((acc, current) => acc + current, 0);
    // acc => 0(初始值) => 1(第一次 0 + 1) => 3(第二次 1 + 2) => 6(第三次 3 + 3) => 10(第四次 6 + 4)
    console.log(sum); // 输出:10
    
  • 示例:计算数组中的最大值

    const numbers = [1, 2, 3, 4];
    const max = numbers.reduce((result, current) => Math.max(result, current), numbers[0])
    console.log(max); // 输出:4
    /*
    初始值 numbers[0]
    - reduce 方法的第二个参数是初始值。在这里,初始值设置为 numbers 数组的 第一个元素。
    - 第一次调用回调函数的时候,result 的值是 numbers[0], 而 current 的值是数组中的第二个元	 素 Numbers[1]
    运行过程
    - reduce 会遍历数组numbers的每一个元素
    	- 第一次: result = numbers[0], current = numbers[1]
        - 第二次: result 更新为 第一次返回的值,current = numbers[2] 
        - 依次类推,直到遍历完成整个数组
        
    示例
    初始值: result = 1
    第一次比较 Math.max(1, 2) => 2
    第二次比较 Math.max(2, 3) => 3
    第三次比较 Math.max(3, 4) => 4
    
    最终, max 的值为 4
    */ 
    

4. reduce 的进阶应用

  • 示例:将数组转化为对象

    const arr = ['a', 'b', 'c'];
    const obj = arr.reduce((acc, current) => {
      acc[current] = current.toUpperCase();
      return acc;
    }, {});
    /*
    代码的功能是小写的key,大写的值
    
    初始值: {}
    第一次: {a: A}
    第二次: {a: A, b: B}
    第三次: {a: A, b: B, c: C}
    */
    console.log(obj); // 输出:{ a: 'A', b: 'B', c: 'C' }
    
  • 示例:扁平化嵌套数组

    const nestedArr = [[1, 2], [3, 4], [5]];
    const flatArr = nestedArr.reduce((acc, current) => acc.concat(current), []);
    
    /*
    代码的功能将数组拍平一层,根据需求可能还要拍平多层
    
    初始值: []
    第一次: [1, 2]
    第二次: [1, 2, 3, 4]
    第三次: [1, 2, 3, 4, 5]
    */
    
    console.log(flatArr); // 输出:[1, 2, 3, 4, 5]
    
  • 示例:统计数组中元素的出现次数

    const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'banana'];
    const count = fruits.reduce((acc, fruit) => {
      acc[fruit] = (acc[fruit] || 0) + 1;
      return acc;
    }, {});
    
    /*
    初始值: {}
    第一次: {apple: 1}
    第二次: {apple: 1, banana: 1}
    第二次: {apple: 2, banana: 1}
    第二次: {apple: 2, banana: 1, orange: 1}
    第二次: {apple: 2, banana: 2, orange: 1}
    第二次: {apple: 2, banana: 3, orange: 1}
    */
    
    console.log(count); // 输出:{ apple: 2, banana: 3, orange: 1 }
    

5. 解决常见难点

  • 初始值的设置:解释初始值的作用和影响。

    • reduce 方法的初始值决定了累加器的初始状态,并且在没有提供初始值的情况下,第一次调用回调函数时,累加器将会使用数组中的第一个元素。

    • 初始值的设置会影响结果,尤其在空数组或某些特定情况下。

      /*
      提供初始值
      */
      const numbers = [1, 2, 3, 4, 5];
      const countNumbers = numbers.reduce((result, current) => result + current, 0);
      console.log(countNumbers); // 输出:15
      // 这个例子是初始值为0,累加器从0开始。
      
      /*
      不提供初始值
      */
      const numbers = [1, 2, 3, 4, 5];
      const countNumbers = numbers.reduce((result, current) => result + current)
      console.log(countNumbers) // 输出15
      // 不提供初始值,累加器从数组的第一个元素1开始,第一次调用是,result是1,current是2
      

      重点:当没有初始值的时候,累加器会从第一个元素开始!

      但是,也会有特例

      // 当为空数组的时候,提供初始值会返回初始值本身,不提供初始值则会报错!
      const emptyArray = [];
      const sumCount = emptyArray.reduce((result, current) => result + current, 0);
      console.log(sumCount); // 输出:0
      
      // 没有初始值会抛出错误
      try {
        const sumCount = emptyArray.reduce((result, current) => result + current);
      } catch (error) {
        console.log(error.message); // 输出:Reduce of empty array with no initial value <直译:reduce 得到了空数组没有初始值>
      }
      
  • 有初始值和无初始值的区别

    • 初始值的来源

      • 有初始值
        • 累加器从提供的初始值开始,无论数组的内容如何。
        • 第一次调用回调函数时,累加器的值为初始值,当前元素为数组的第一个元素。
      • 无初始值
        • 累加器的初始值为数组的第一个元素,第一次调用回调函数的时,累加器的值是数组的第一个元素,当前元素是第二个元素。
        • 如果数组为空,调用reduce将会报错
    • 处理空数组

      • 有初始值

        • 如果数组为空,reduce将直接返回初始值

          const result = [].reduce((acc, current) => acc + current, 0);
          console.log(result); // 输出:0
          
      • 无初始值

        • 如果数组为空,reduce 将会抛出错误,因为没有元素可供使用

          try {
            const result = [].reduce((acc, current) => acc + current);
          } catch (error) {
            console.log(error.message); // 输出:Reduce of empty array with no initial value
          }
          
    • 结果的计算

      • 有初始值

        • 结果可能与初始值相关,例如在求和时,可以将初始值设置为10,则最终结果会是数组和10的总和

          const numbers = [1, 2, 3];
          const countNumbers = numbers.reduce((result, current) => result + current, 10);
          console.log(countNumbers); // 输出:16 (10 + 1 + 2 + 3)
          
      • 无初始值

        • 结果完全依赖于数组的内容。如果数组只有一个元素,结果就是这个元素;如果数组为空,则会抛出错误。

          const numbers = [1, 2, 3];
          const countNumbers = numbers.reduce((result, current) => result + current);
          console.log(countNumbers); // 输出:6 (1 + 2 + 3)
          
    • 总结

      有初始值: 更加灵活,能安全处理空数组,并且可以影响最终结果。

      无初始值:简单且清晰,但在某些情况下(如空数组)会导致报错。

      这里呢,小编建议总是加上初始值,因为这样对排错、代码更加友好,思路更清晰

  • reduceforEach 的对比

    • 目的和使用场景

      • reduce

        • 旨在从数组中生成一个单一的值(如数字、对象、数组等)

        • 通常用于聚合、变换或计算的操作

        • 例子:计算总和、扁平化数组、统计频率等

      • forEach

        • 用于遍历数组中的每个元素,执行副作用操作(如打印、修改外部变量等)

        • 不返回值,不能直接用于生成结果。

    • 返回值

      • reduce

        • 返回最终的累加值

        • 每次调用返回的值作为下次迭代的累加器

      • forEach

        • 返回 Undefined,无法直接用作数组生成。

          const numbers = [1, 2, 3];
          // 需要外部变量辅佐新的数组生成
          let sum = 0;
          numbers.forEach(num => {
              sum += num;
          })
          console.log(sum) // 输出: 6
          
    • 链式调用

      • reduce

        • 可以与其他数组方法链式调用,形成更复杂的操作。

          const numbers = [1, 2, 3];
          const result = numbers.map(num => num * 2)
          					  .reduce((pre, curr) => pre + curr, 0);
          console.log(result)
          
      • forEach

        • 由于不返回值,无法进行链式调用。
    • 总结

      • reduce: 用于从数组生成单一值,支持复杂数据处理和链式调用。
      • forEach:用于遍历数组并执行操作,不返回结果,适合简单的副作用。
  • 典型错误及其解决方案

    • 未考虑初始值

      • 错误:在处理空数组时未提供初始值,导致运行时错误。

        const result = [].reduce((pre, curr) => pre + curr) // 报错
        
      • 解决方案: 总是为 reduce 提供一个初始值,确保即使数组为空时,也能得到期望结果。

        const result = [].reduce((pre, curr) => pre + curr, 0) // 输出: 0
        
    • 累加器未返回

      • 错误:在回调函数中未返回累加器,导致最终结果不正确。

        const numbers = [1, 2, 3];
        const sum = numbers.reduce((pre, curr) => {
            pre += curr // 忘记返回
        }, 0)
        console.log(sum) // 输出: undefined
        
      • 解决方案: 确保每次迭代都显式返回累加器

        const numbers = [1, 2, 3];
        const sum = numbers.reduce((pre, curr) => {
            return pre += curr // 返回累加器
        }, 0)
        console.log(sum) // 输出: 6
        
    • 错误的数据类型

      • 错误:累加器与当前元素类型不匹配,导致结果错误。

        const number = [1, '2', 3];
        const sum = number.reduce((pre, curr) => pre + curr, 0) // 结果为字符串
        console.log(sum) // 输出: '123'
        
      • 解决方案:确保在处理前对数据进行适当的类型转换

        const number = [1, '2', 3];
        const sum = number.reduce((pre, curr) => pre + Number(curr), 0)
        console.log(sum) // 输出: 6
        

6. 实际应用场景

  • 示例项目:使用 reduce 进行统计商品销售数据

    const salesData = [
      { productId: 'A', amount: 100 },
      { productId: 'B', amount: 150 },
      { productId: 'A', amount: 200 },
      { productId: 'C', amount: 300 },
      { productId: 'B', amount: 100 },
      { productId: 'C', amount: 250 },
    ];
    
    // 使用 reduce 进行汇总
    const totalSales = salesData.reduce((acc, current) => {
      // 检查当前商品 ID 是否已在累加器中
      if (!acc[current.productId]) {
        acc[current.productId] = 0; // 初始化总销售额
      }
      acc[current.productId] += current.amount; // 累加销售额
      return acc; // 返回累加器
    }, {});
    
    console.log(totalSales);
    // 输出:{ A: 300, B: 250, C: 550 }
    
  • 上诉代码中,可以统计的数据通常会很多,这里只是一种情况,多数据的情况下可以自行拓展

    • 例如: acc[current.productId + 'Count'] += 1
    • 例如: acc[current.productAmount + 'Count'] += current.amount