📕 重学JavaScript:如何手写一个`reduce`高阶函数?

简介: `reduce` 高阶函数是一个非常常用的数组方法,可以让你用一种简单的方法来处理数组里的元素。 数组就是一串有顺序的东西,比如[1, 2, 3, 4]就是一个数组,里面有四个数字。👌

📕 重学JavaScript:如何手写一个reduce高阶函数?

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️

reduce 高阶函数是一个非常常用的数组方法,可以让你用一种简单的方法来处理数组里的元素。 数组就是一串有顺序的东西,比如[1, 2, 3, 4]就是一个数组,里面有四个数字。👌

reduce可以让你把数组里的元素按照你想要的规则合并成一个值。

比如,你可以用reduce来算出数组里所有数字加起来是多少,或者乘起来是多少,或者哪个数字最大,哪个数字最小等等。😊

在ECMA中,也就是一群很聪明的人制定的一些规则中,他们已经给我们解释了reduce方法是怎么工作的。

他们用了一段伪代码,也就是一种类似于真正代码但是不完全一样的东西,来描述reduce方法的每一个步骤:

1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callbackfn) is false, throw a TypeError exception.
4. If len = 0 and initialValue is not present, throw a TypeError exception.
5. Let k be 0.
6. Let accumulator be undefined.
7. If initialValue is present, then
a. Set accumulator to initialValue.
8. Else,
a. Let kPresent be false.
b. Repeat, while kPresent is false and k < len,
i. Let Pk be ! ToString(𝔽(k)).
ii. Set kPresent to ? HasProperty(O, Pk).
iii. If kPresent is true, then
1. Set accumulator to ? Get(O, Pk).
iv. Set k to k + 1.
c. If kPresent is false, throw a TypeError exception.
9. Repeat, while k < len,
a. Let Pk be ! ToString(𝔽(k)).
b. Let kPresent be ? HasProperty(O, Pk).
c. If kPresent is true, then
i. Let kValue be ? Get(O, Pk).
ii. Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, 𝔽(k), O »).
d. Set k to k + 1.
10. Return accumulator.

这段伪代码看起来好像还很复杂的?我们一步一步来解析一下:

  • 把调用reduce函数的值转换成一个对象(ToObject)
  • 获取对象的长度(LengthOfArrayLike)
  • 判断传入的回调函数是否是一个函数(IsCallable)
  • 检查一下对象是不是空的,并且有没有给初始值(initialValue)
  • 把一个变量叫做k设为0。
  • ……啊,算了不翻译了,你们自己看吧

好的,根据这个原理,我们可以用JavaScript来实现我们的目标。😎

⭕ 解答一部分实现的关键逻辑

把调用reduce函数的值转换成一个对象(ToObject)。

保证 reduce 函数可以接受任何类型的值作为参数,比如字符串、数字、布尔等,不会因为用户传入的差异导致报错。

// 假设我们有一个数组arr
let arr = [1, 2, 3, 4];
// 把它转换成一个对象O
let O = Object(arr);

获取对象的长度(LengthOfArrayLike)

确定我们需要遍历对象中的多少个元素

// 获取对象O的所有属性名
let keys = Object.keys(O);
// 获取它们的个数,也就是对象O的长度len
let len = keys.length;

判断传入的回调函数是否是一个函数(IsCallable)

保证我们传入的回调函数是一个有效的函数,而不是其他类型的值

我们使用 typeof 运算符来判断回调函数的类型是否是 "function"

如果不是,我们就抛出一个类型错误

  if (typeof callbackfn !== "function") {
   
    throw new TypeError(callbackfn + " is not a function");
  }

检查一下对象是不是空的,并且有没有给初始值(initialValue)

我们可以用len变量来检查对象是否为空,如果len是0,就表示对象为空

用arguments对象来检查是否有给初始值,如果arguments.length是2,就表示有给初始值

// 假设我们有一个初始值initialValue
let initialValue = 10;
// 检查对象O是否为空,并且是否有给初始值
if (len === 0 && arguments.length < 2) {
   
  // 如果是,就报错
  throw new TypeError("Reduce of empty array with no initial value");
}

检查对象里有没有Pk这个属性

// 检查一下对象里有没有Pk这个属性,如果有,就把kPresent设为true
if (O.hasOwnProperty(Pk)) {
   
    kPresent = true;
}

其他的都是口水话,很简单,这里我就不过多解释了,接下来直接上源码。

💌 完整代码解析如下:

function reduce(callbackfn, initialValue) {
   
  // 把调用reduce函数的值转换成一个对象
  let O = Object(this);
  // 把对象的长度记下来
  let len = Object.keys(O).length;
  // 判断传入的回调函数是否是一个函数,如果不是,就报错
  if (typeof callbackfn !== "function") {
   
    throw new TypeError(`${
     callbackfn} is not a function`);
  }
  // 检查对象是否为空,并且是否有给初始值,如果没有,就报错
  if (len === 0 && arguments.length < 2) {
   
    throw new TypeError("Reduce of empty array with no initial value");
  }
  // 把一个变量叫做k设为0
  let k = 0;
  // 把一个变量叫做累积器设为undefined
  let accumulator = undefined;
  // 如果有给初始值,就把累积器设为初始值
  if (arguments.length >= 2) {
   
    accumulator = initialValue;
  }
  // 如果没有给初始值,就做以下事情
  else {
   
    // 把一个变量叫做kPresent设为false
    let kPresent = false;
    // 重复以下步骤,直到找到要处理的元素或者处理完了所有元素
    while (kPresent === false && k < len) {
   
      // 把k转换成字符串,并叫做Pk
      let Pk = String(k);
      // 检查一下对象里有没有Pk这个属性,如果有,就把kPresent设为true
      if (O.hasOwnProperty(Pk)) {
   
        kPresent = true;
        // 如果kPresent是true,就把对象里Pk这个属性对应的值赋给累积器
        accumulator = O[Pk];
      }
      // 把k加1
      k++;
    }
    // 如果kPresent是false,就报错
    if (kPresent === false) {
   
      throw new TypeError("Reduce of empty array with no initial value");
    }
  }
  // 重复以下步骤,直到处理完了所有元素
  while (k < len) {
   
    // 把k转换成字符串,并叫做Pk
    let Pk = String(k);
    // 检查一下对象里有没有Pk这个属性,如果有,就把kPresent设为true
    if (O.hasOwnProperty(Pk)) {
   
      kPresent = true;
      // 如果kPresent是true,就做以下事情
      if (kPresent === true) {
   
        // 把对象里Pk这个属性对应的值赋给一个变量叫做kValue
        let kValue = O[Pk];
        // 调用回调函数,把累积器、kValue、k和对象作为参数传进去,并把返回值赋给累积器
        accumulator = callbackfn(accumulator, kValue, k, O);
      }
    }
    // 把k加1
    k++;
  }
  // 返回累积器
  return accumulator;
}

其中有几个核心要点:

  1. 初始值不传怎么处理
  2. 回调函数的参数有哪些,返回值如何处理。

💨 我们试一下是否能实现reduce函数的效果

let arr = [1, 2, 3, 4];
let callbackfn = function(a, b) {
   return a + b};
let initialValue = 10;

let result = reduce.call(arr, callbackfn, initialValue);
// 输出结果,应该是20(1 + 2 + 3 + 4 + 10)
console.log(result);

✔ V8引擎中的map源码是怎么实现的呢?

我在这里直接粘贴给大家看看,大家自行对比一下:

function ArrayReduce(callback, current) {
   
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.reduce");

  // Pull out the length so that modifications to the length in the
  // loop will not affect the looping and side effects are visible.
  var array = TO_OBJECT(this);
  var length = TO_LENGTH(array.length);
  return InnerArrayReduce(callback, current, array, length,
                          arguments.length);
}

function InnerArrayReduce(callback, current, array, length, argumentsLength) {
   
  if (!IS_CALLABLE(callback)) {
   
    throw %make_type_error(kCalledNonCallable, callback);
  }

  var i = 0;
  find_initial: if (argumentsLength < 2) {
   
    for (; i < length; i++) {
   
      if (i in array) {
   
        current = array[i++];
        break find_initial;
      }
    }
    throw %make_type_error(kReduceNoInitial);
  }

  for (; i < length; i++) {
   
    if (i in array) {
   
      var element = array[i];
      current = callback(current, element, i, array);
    }
  }
  return current;
}

应该还算实现得完整吧😄。

📕 参考资料

V8源码

ecma262草案


🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨

目录
相关文章
|
1月前
|
JavaScript 前端开发
解释 JavaScript 中的`map()`、`filter()`和`reduce()`方法的用途。
解释 JavaScript 中的`map()`、`filter()`和`reduce()`方法的用途。
19 1
|
3月前
|
JavaScript
js中数组reduce的使用原来这么简单
js中数组reduce的使用原来这么简单
|
6月前
|
前端开发 JavaScript
前端基础 - JavaScript高级应用(高阶函数)
前端基础 - JavaScript高级应用(高阶函数)
31 0
|
9月前
|
存储 JavaScript 前端开发
js数组高阶函数——includes()方法
js数组高阶函数——includes()方法
192 0
|
3月前
|
分布式计算 JavaScript 前端开发
JS中数组22种常用API总结,slice、splice、map、reduce、shift、filter、indexOf......
JS中数组22种常用API总结,slice、splice、map、reduce、shift、filter、indexOf......
|
20天前
|
JavaScript 安全 前端开发
高阶函数(js的问题)
高阶函数(js的问题)
|
7月前
|
存储 JavaScript 前端开发
js中的遍历方法比较:map、for...in、for...of、reduce和forEach的特点与适用场景
js中的遍历方法比较:map、for...in、for...of、reduce和forEach的特点与适用场景
|
3月前
|
JavaScript 前端开发 Java
【面试题】JS的一些优雅写法 reduce和map
【面试题】JS的一些优雅写法 reduce和map
|
6月前
|
存储 JavaScript 前端开发
JS的for循环,forin循环,forof循环,foreach循环map循环以及,reduce()循环 方法最实用详解。
JS的for循环,forin循环,forof循环,foreach循环map循环以及,reduce()循环 方法最实用详解。
|
8月前
|
JavaScript
js数组求和(通过reduce方法)
js数组求和(通过reduce方法)
64 0