Array.reduce()的秘密

427 阅读5分钟

原理

大部分现代的数组方法都返回一个新的数组,而Array.reduce() 更加灵活。它可以返回任意值,它的功能就是将一个数组的内容聚合成单个值,这个值可以是数字、字符串,甚至可以是对象或新数组。

用法

Array.reduce()接受两个参数:一个是对数组每个元素执行的回调方法,一个是初始值。

这个回调接收4个参数,前两个参数是:accumulator是当前聚合值,current是数组循环时的当前元素。无论你返回什么值,都将作为累加器提供给循环中的下一个元素。初始值将作为第一次循环的累加器。

var myNewArray = [].reduce(function (accumulator, current) {
  return accumulator;
}, starting);

1. 数组求和

假设你想把一组数字加在一起。使用Array.forEach()大概可以这么做:

var total = 0;

[1, 2, 3].forEach(function (num) {
  total += num;
});

这是Array.reduce()用得最多的例子了。我发现accumulator这个单词让人困惑,所以在示例中我改为sum,因为这里就是求和的意思。

var total = [1, 2, 3].reduce(function (sum, current) {
  return sum + current;
}, 0);

这里传入0作为初始值。 在回调里,将当前值加入到sum,第一轮循环时它的值是初始值0,然后变成1(初始值0加上当前元素值1),然后变成3(累加值1加上当前元素值2),以此类推

2. 组合多个数组方法

假设有一个wizards数组:

var wizards = [
  {
    name: 'Harry Potter',
    house: 'Gryfindor'
  },
  {
    name: 'Cedric Diggory',
    house: 'Hufflepuff'
  },
  {
    name: 'Tonks',
    house: 'Hufflepuff'
  },
  {
    name: 'Ronald Weasley',
    house: 'Gryfindor'
  },
  {
    name: 'Hermione Granger',
    house: 'Gryfindor'
  }
];

你想创建一个仅包含住在 Hufflepuff 的巫师名字的新数组。一个可行的方法是使用Array.filter()方法获取house属性为Hufflepuffwizards。然后用Array.map()方法创建一个只包含过滤后对象的name属性的新数组。

var hufflepuff = wizards.filter(function (wizard) {
  return wizard.house === 'Hufflepuff';
}).map(function (wizard) {
  return wizard.name;
});

使用Array.reduce()方法,我们可以用一步得到同样的结果,提高了性能。传递一个空数组[]作为初始值。每次循环时判断wizard.house是否为Hufflepuff。如果是,就加入到newAr中(即accumulator),否则啥也不做。

无论判断条件是否成立,最后都返回newArr作为下一次循环的accumulator

var hufflepuff = wizards.reduce(function (newArr, wizard) {
  if (wizard.house === 'Hufflepuff') {
    newArr.push(wizard.name);
  }
  return newArr;
}, []);

3. 从数组生成 HTML 标签

那么,如果想创建一个由住在 Hufflepuff 的巫师组成的无序列表要怎么做呢?这次不是给Array.reduce()传一个空数组作为初始值了,而是一个名为html的空字符串''

如果wizard.house等于Hufflepuff,我们就将wizard.name用列表项li包裹起来,再拼接到html字符串里。然后返回html作为下一次循环的accumulator

var hufflepuffList = wizards.reduce(function (html, wizard) {
  if (wizard.house === 'Hufflepuff') {
    html += '<li>' + wizard.name + '</li>';
  }
  return html;
}, '');

Array.reduce()前后添加无序列表的开始和结束标记,就可以把它插入到 DOM 中了。

var hufflepuffList = '<ul>' + wizards.reduce(function (html, wizard) {
  if (wizard.house === 'Hufflepuff') {
    html += '<li>' + wizard.name + '</li>';
  }
  return html;
}, '') + '</ul>';

4. 数组元素分组

lodash 有个groupBy()方法,可以将数组元素按照某个标准分组。

假设你有一个数字数组。

如果你想把numbers数组中的元素按照整数部分的值分组,用 lodash可以这样做:

var numbers = [6.1, 4.2, 6.3];

// 返回 {'4': [4.2], '6': [6.1, 6.3]}
_.groupBy(numbers, Math.floor);

如果你有一个单词数组,你想根据words中的单词长度分组,你可以这样做:

var words = ['one', 'two', 'three'];

// 返回 {'3': ['one', 'two'], '5': ['three']}
_.groupBy(words, 'length');

Array.reduce() 实现groupBy()函数

你可以用Array.reduce()方法实现同样的功能。

我们来创建一个工具函数groupBy(),接受数组和分组条件作为参数。在groupBy()内部,在数组上执行Array.reduce(),传一个空对象{}作为初始值,然后返回结果。

var groupBy = function (arr, criteria) {
  return arr.reduce(function (obj, item) {
    // 省略代码
  }, {});
};

Array.reduce()回调函数内部,我们会判断criteria是函数还是item的属性。然后获取当前item的值。

如果obj中还不存在这个属性,则创建它,并将一个空数组赋值给它。最后,将item添加到key的数组中,再返回该对象作为下一次循环的accumulator

var groupBy = function (arr, criteria) {
  return arr.reduce(function (obj, item) {

    // 判断criteria是函数还是属性名
    var key = typeof criteria === 'function' ? criteria(item) : item[criteria];

    // 如果属性不存在,则创建一个
    if (!obj.hasOwnProperty(key)) {
      obj[key] = [];
    }

    // 将元素加入数组
    obj[key].push(item);

    // 返回这个对象
    return obj;

  }, {});
};

5. 合并数据到单个数组

还记得前面的wizards数组吗?

var wizards = [
  {
    name: 'Harry Potter',
    house: 'Gryfindor'
  },
  {
    name: 'Cedric Diggory',
    house: 'Hufflepuff'
  },
  {
    name: 'Tonks',
    house: 'Hufflepuff'
  },
  {
    name: 'Ronald Weasley',
    house: 'Gryfindor'
  },
  {
    name: 'Hermione Granger',
    house: 'Gryfindor'
  }
];

如果还有另一份数据,每个巫师获得的的积分对象:

var points = {
  HarryPotter: 500,
  CedricDiggory: 750,
  RonaldWeasley: 100,
  HermioneGranger: 1270
};

假设你想把两份数据合并到一个数组,也就是把points数值添加到每个巫师对象上。你会怎么做?

Array.reduce()方法特别适合!

var wizardsWithPoints = wizards.reduce(function (arr, wizard) {

  // 移除巫师名字中的空格,用来获取对应的 points
  var key = wizard.name.replace(' ', '');

  // 如果wizard有points,则加上它,否则设置为0
  if (points[key]) {
    wizard.points = points[key];
  } else {
    wizard.points = 0;
  }

  // 把wizard对象加入到新数组里
  arr.push(wizard);

  // 返回这个数组
  return arr;

}, []);

其实这里用Array.map也很方便实现。

6. 合并数据到单个对象

如果你想合并两个来源的数据到一个对象中,也就是巫师的名字作为属性名,house 和 points 作为属性值,要怎么做呢?同样,Array.reduce()很合适。

var wizardsAsAnObject = wizards.reduce(function (obj, wizard) {

  // 移除巫师名字中的空格,用来获取对应的 points
  var key = wizard.name.replace(' ', '');

  // 如果wizard有points,则加上它,否则设置为0
  if (points[key]) {
    wizard.points = points[key];
  } else {
    wizard.points = 0;
  }

  // 删除 name 属性
  delete wizard.name;

  // 把 wizard 数据添加到新对象中
  obj[key] = wizard;

  // 返回该对象
  return obj;

}, {});

总结:Array.reduce()真香

Array.reduce()方法有着良好的浏览器支持。所有的现代浏览器都支持,包括 IE9 及以上。移动端浏览器也在很早之前就支持了。如果你还需要支持更老的浏览器,可以添加一个 polyfill 来支持到 IE6。

Array.reduce()最大的槽点可能就是对于从来没接触过的人来说有点费解。组合使用Array.filter()Array.map()执行起来更慢,并且包含多余的步骤,但是更容易阅读,从方法名可以明显看出它要做的事情。