Array.prototype.reduce的实现解读

发表于 2019-11-04
更新于 2024-05-23
分类于 技术专栏
阅读量 9398
字数统计 3859

本文旨在学习reduce函数实现以及应用,参考的是MDN的polyfill。

功能实现的解读

Array.prototype.reduce = function(callback, /*initValue*/) {
  // 这里其实只是判断了null,并没有判断undefined,所以这里的报错的语句不是那么精确
  if (this === null) {
    throw new TypeError( 'Array.prototype.reduce ' +
          'called on null or undefined' );
  }

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

  var o = Object(this) // 1️⃣

  var len = this.length >>> 0 // 2️⃣

  var k = 0
  var initValue

  // 如果参数大于2个,说明传递了初始值,根据reduce函数的定义,如果传递了初始值,那么就从初始值还是执行回调函数,
  // 如果没有初始值,则将数组的第一个值作为初始值
  if (arguments.length >= 2) {
    initValue = arguments[1]
  } else {
    // 没有传初始值的情况,那么要计算初始值
    while(k < len && !(k in o)) { // 3️⃣
      k++
    }

    if (k > len) {
      throw new TypeError( 'Reduce of empty array ' +
            'with no initial value' );
    }
    value = o[k++]; // 4️⃣
  }

  while (k < len) {
    if ( k in o) {
      value = callback(value, o[k], k, o)
    }
    k++
  }
  return value
}

上述代码引自MDN上的实现,实现中有上面四处代码显得诡异,没有那么容易看懂。我们一一解释:

1、Object(this):这是为了转换this等于undefined的情况,最开始的if判断只是判断了null,而如果是undefined的话,通过该语句之后o就等于了{},所以加这个只是为了传undefined的时候不会报错

2、var len = this.length >>> 0:这是为了保证len是正整数,虽然this.length肯定不会为负数,也不会是别的类型,但可能为了考虑全面吧,防止有意外~

话说,很多人不知道`>>>`和`>>`的区别:前者将数字右移指定位数,然后首位永远添加的是0,所以一个负数使用这个之后就会变为正数: -9 >>> 2 => 1073741821,而后者右移的时候不会去动到符号位:-9 >> 2 => -3

3、while循环计算k值:这是为了处理稀疏数组,什么是稀疏数组呢?比如下面的代码:

var a = []
a[0] = 1
a[1] = 2
a[3] = 3

那么中间的下标2的值是empty,这种就是稀疏数组,在浏览器打印出来一般是这样的:[1, 2, empty, 3],这种与[1, 2, undefined, 99]不一样的,所以在执行k in o的时候就不一样:2 in [1, 2, empty, 3]结果为false,而2 in [1, 2, undefined, 99]结果为true。

因此while循环过滤掉所有是empty值的元素。

因此如果将上面的数组进行reduce:[1, 2, empty, 3].reduce((sum, val) => {}),只会执行回调两次(为什么?看看代码就知道了~)

4、value = o[k++]:赋初值之后,k需要自增1,因为o这个数组的第一个有效元素已经作为累加值的起始值了,所以不需要考虑该值。在reduce中还有一个点:

当数组是空的时候,但是提供了初始值的话,回调函数不会被调用

当数组只有一个有效元素,但是没有提供初始值,回调函数也不会被调用

reduce的常用应用

数组累加

var total = [ 0, 1, 2, 3 ].reduce(
  ( accumulator, currentValue ) => accumulator + currentValue,
  0
);

转换二维数组为一维数组(flattern)

var flattened = [[0, 1], [2, 3], [4, 5]].reduce(
  ( accumulator, currentValue ) => accumulator.concat(currentValue),
  []
);

统计数组某个词语出现的次数

var names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

var countedNames = names.reduce(function (allNames, name) {
  if (name in allNames) {
    allNames[name]++;
  }
  else {
    allNames[name] = 1;
  }
  return allNames;
}, {});
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }

根据这个用法,还可以去删除重复的元素。

对象中根据某个属性分类

var people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// {
//   20: [
//     { name: 'Max', age: 20 },
//     { name: 'Jane', age: 20 }
//   ],
//   21: [{ name: 'Alice', age: 21 }]
// }

函数组合

// Building-blocks to use for composition
const double = x => x + x;
const triple = x => 3 * x;
const quadruple = x => 4 * x;

// Function composition enabling pipe functionality
const pipe = (...functions) => input => functions.reduce(
    (acc, fn) => fn(acc),
    input
);

// Composed functions for multiplication of specific values
const multiply6 = pipe(double, triple);
const multiply9 = pipe(triple, triple);
const multiply16 = pipe(quadruple, quadruple);
const multiply24 = pipe(double, triple, quadruple);

// Usage
multiply6(6); // 36
multiply9(9); // 81
multiply16(16); // 256
multiply24(10); // 240

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 Array.prototype.reduce的实现解读 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github