Array.prototype.reduce 实用指南

部分内容引自原文地址
在原文基础上有增删改。

Array.prototype.reduce 算是 JavaScript 数组中比较难用但又特别强大的方法。

本文第一部分以实用为主,通过例子展示如何使用这个方法。
第二部分介绍一下reduce方法的本质和异常处理。

一、简介 & 例子

Array.prototype.reduce 简介

reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,将其简化为单个值。

上述是 MDN对该方法的描述,方法的语法是:
arr.reduce(callback[, initialValue])
callback 接受四个参数,分别是:
accumulator,累加器累加回调的返回值;
currentValue,数组中正在处理的元素;
currentIndex(可选),数组中正在处理的当前元素的索引;
array(可选),调用 reduce() 的数组。
initialValue 为可选参数,作为第一次调用 callback 函数时的第一个参数的值。方法的返回值是函数累计处理的结果。

一股脑介绍完之后,估计不少同学都是比较懵的。其实这个方法并不难理解的,正如它名字所示,抓住它的核心:聚合
一般而言,如果需要把数组转换成其他元素,如字符串、数字、对象甚至是一个新数组的时候,若其他数组方法不太适用时,就可以考虑 reduce 方法,不熟悉这个方法的同学,尽管抛开上面的语法, 记住方法的核心是聚合即可。

下文的例子都用到以下数组,假设通过接口获取到如下的数据体:

[{
  id: 1,
  type: 'A',
  total: 3
}, {
  id: 2,
  type: 'B',
  total: 5
}, {
  id: 3,
  type: 'E',
  total: 7
},...]

数据体是按照 id 的升序进行排列,totaltype 不定~

聚合为数字

根据上述数据体,我们一起来做第一个小需求,统计 total 的总和。如果不用 reduce,其实也不难:

function sum(arr) {
  let sum = 0;
  for (let i = 0, len = arr.length; i < len; i++) {
    const { total } = arr[i];
    sum += total;
  }
  return sum;
}

这个函数可以完成上述需求,但我们精确地维护了数组索引,再精确地处理整个运算过程,是典型的命令式编程。上文提及,只要涉及将数组转换为另外的数据体,就可以使用 reduce,它可以这样写:

arr.reduce((sum, { total }) => {
  return sum + total;
}, 0)

这样就完成了~sum 是此前累加的结果,它的初始值为 0。每次将此前的累计值加上当前项的 total 为此次回调函数的返回值,作为下次执行时 sum 的实参使用。看起来比较绕,可以参考下面的表格:

轮次 sum total 返回值
1 0(初始值) 3 3
2 3 5 8
3 8 7 15
... ... ... ...

如此是不是清晰了很多?前一次的返回值就是后一次 sum 的值,如此类推,最后累积出总和,将数组聚合成了数字。

聚合为字符串

下一个需求是将数组的每项转换为固定格式的字符串(如第一项转换为 id:1,type:A;),每项直接以分号作为分隔。一般来说,数组转为字符串,join 方法是不错的选择,但并不适用于需要精确控制或数组的项比较复杂的情况。在本例中,join 方法是达不到我们想要的效果的。

使用 for 循环当然可以解决问题,但 reduce 也许是更好的选择,代码如下:

arr.reduce((str, { id, type }) => {
  return str + `id:${id},type:${type};`;
}, '')

有了聚合为数字的例子,这次你能在脑海中模拟出执行的过程么?以下也是前三项的执行过程:

轮次 str id type 返回值
1 ''(初始值) 1 'A' 'id:1,type:A;'
2 'id:1,type:A;' 2 'B' 'id:1,type:A;id:2,type:B;'
3 'id:1,type:A;id:2,type:B;' 3 'E' 'id:1,type:A;id:2,type:B;id:3,type:E;'
... ... ... ... ...

聚合为对象

有了前面的一点基础,可以做复杂一点的聚合了。上面的数据体是比较典型的后端接口返回结果,但对于前端来说,转换成 key value 的对象形式,更利于进行之后的操作。那我们就以转换为 keyidvalue 是其他属性的对象作为目标吧!

function changeToObj(arr) {
  const res = {};
  arr.forEach(({ id, type, total }) => {
    res[id] = {
      type,
      total
    };
  })
  return res;
}

如上所示,这个函数可以很好地完成我们的目标。但略显啰嗦,记住:只要目标是将数组聚合为唯一的元素时,都可以考虑使用 reduce。这个例子恰好符合:

arr.reduce((res, { id, type, total }) => {
  res[id] = {
    type,
    total
  };
  return res;
}, {})

res 是最后返回的对象,通过遍历数组,不断往里面添加新的属性与值,最后达到聚合成对象的目的,代码还是相当简洁有力的。

最后,对于不熟悉这个方法的同学,不妨练习一下,将数据体转换为一个字符串数组,数组每一项为原数组 type 的值。

注意事项

  1. 提供初始值是个好习惯。如果想对数组中的每一项进行一下数据处理后再返回,需保证每一次都是从第0项开始调用,所以此时务必提供reduce函数的第二个参数-初始值。否则会从第1项开始调用,第0项反应在callback的第一个参数中
  1. callback函数中return必不可少。否则无论有没有提供初始值,callback函数的第一个参数永远是undefined

二、异常处理 & polyfill

异常情况

  1. 空数组
let a = [];
a.reduce(() => {}, 1);     // 1

a.reduce(() => {});    //  Uncaught TypeError: Reduce of empty array with no initial value at Array.reduce

这也更加印证了上面提到的那点,提供初始值是个好习惯

Polyfill

// Production steps of ECMA-262, Edition 5, 15.4.4.21
// Reference: http://es5.github.io/#x15.4.4.21
// https://tc39.github.io/ecma262/#sec-array.prototype.reduce
if (!Array.prototype.reduce) {
  Object.defineProperty(Array.prototype, 'reduce', {
    value: function(callback /*, initialValue*/) {
      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');
      }

      // 1. Let O be ? ToObject(this value).
      var o = Object(this);

      // 2. Let len be ? ToLength(? Get(O, "length")).
      var len = o.length >>> 0; 

      // Steps 3, 4, 5, 6, 7      
      var k = 0; 
      var value;

      if (arguments.length >= 2) {
        value = arguments[1];
      } else {
        while (k < len && !(k in o)) {
          k++; 
        }

        // 3. If len is 0 and initialValue is not present,
        //    throw a TypeError exception.
        if (k >= len) {
          throw new TypeError( 'Reduce of empty array ' +
            'with no initial value' );
        }
        value = o[k++];
      }

      // 8. Repeat, while k < len
      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. Let accumulator be ? Call(
        //          callbackfn, undefined,
        //          « accumulator, kValue, k, O »).
        if (k in o) {
          value = callback(value, o[k], k, o);
        }

        // d. Increase k by 1.      
        k++;
      }

      // 9. Return accumulator.
      return value;
    }
  });
}

其中 第三步while循环,是为了排除空值

let arr = Array(2);
console.log(0 in arr); // false
console.log(1 in arr); // false
console.log(arr.length); // 2

第八步while循环中调用callback,同样是避免循环空值

if (k in o) {
   value = callback(value, o[k], k, o);
}
空值被跳过

小结

以上就是本文的全部内容。原则上说,只要是将数组聚合为唯一的元素时,都可以使用它。同时,它在函数式编程中有一席之地,也是声明式编程的典型例子。这也意味着它不容易掌握,如果熟悉 reduce 方法,写出来的代码可读性强,十分优雅。但在不熟悉的同学眼里,这就是不折不扣的天书了。如何更好地使用 reduce,避免写出难以维护的代码,值得每一位同学思考。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容