Array.prototype.reduce 的理解与实现

Array.prototype.reduce 是 JavaScript 中比较实用的一个函数,可是不少人都没有使用过它,由于 reduce 能作的事情其实 forEach 或者 map 函数也能作,并且比 reduce 好理解。可是 reduce 函数仍是值得去了解的。数组

reduce 函数能够对一个数组进行遍历,而后返回一个累计值,它使用起来比较灵活,下面了解一下它的用法。函数

reduce 接受两个参数,第二个参数可选:ui

@param {Function} callback 迭代数组时,求累计值的回调函数
@param {Any} initVal 初始值,可选
复制代码

其中,callback 函数能够接受四个参数:spa

@param {Any} acc 累计值
@param {Any} val 当前遍历的值
@param {Number} key 当前遍历值的索引
@param {Array} arr 当前遍历的数组
复制代码

callback 接受这四个参数,通过处理后返回新的累计值,而这个累计值会做为新的 acc 传递给下一个 callback 处理。直处处理完全部的数组项。获得一个最终的累计值。prototype

reduce 接受的第二个参数是一个初始值,它是可选的。若是咱们传递了初始值,那么它会做为 acc 传递给第一个 callback,此时 callback 的第二个参数 val 是数组的第一项;若是咱们没有传递初始值给 reduce,那么数组的第一项会做为累计值传递给 callback,数组的第二项会做为当前项传递给 callback。code

示例:对象

对数组求和:索引

let arr = [1, 2, 3];
let res = arr.reduce((acc, v) => acc + v);
console.log(res); // 6
复制代码

若是咱们传递一个初始值:ip

let arr = [1, 2, 3];
let res = arr.reduce((acc, v) => acc + v, 94);
console.log(res); // 100
复制代码

利用 reduce 求和比 forEach 更加简单,代码也更加优雅,只须要清楚 callback 接受哪些参数,表明什么含义就能够了。underscore

咱们还能够利用 reduce 作一些其余的事情,好比对数组去重:

let arr = [1, 1, 1, 2, 3, 3, 4, 3, 2, 4];
let res = arr.reduce((acc, v) => {
  if (acc.indexOf(v) < 0) acc.push(v);
  return acc;
}, []);
console.log(res); // [1, 2, 3, 4]
复制代码

统计数组中每一项出现的次数:

let arr = ['Jerry', 'Tom', 'Jerry', 'Cat', 'Mouse', 'Mouse'];
let res = arr.reduce((acc, v) => {
  if (acc[v] === void 0) acc[v] = 1;
  else acc[v]++;
  return acc;
}, {});
console.log(res); // {Jerry: 2, Tom: 1, Cat: 1, Mouse: 2}
复制代码

将二维数组展开成一维数组:

let arr = [[1, 2, 3], 3, 4, [3, 5]];
let res = arr.reduce((acc, v) => {
  if (v instanceof Array) {
    return [...acc, ...v];
  } else {
    return [...acc, v];
  }
});
console.log(res); // [1, 2, 3, 3, 4, 3, 5]
复制代码

由此能够看出,reduce 函数仍是很实用的,可是 reduce 函数兼容性不是特别好,只支持到 IE 9,若是要在 IE 8 及如下使用的话就不行了,因此咱们能够本身实现一下,还能够对其作一下扩展,使其可以遍历对象。

首先能够实现一个最基础的 each 函数,做为咱们 reduce 的基础:

/** * 遍历对象或数组,对操做对象的属性或元素作处理 * @param {Object|Array} param 要遍历的对象或数组 * @param {Function} callback 回调函数 */
function each(param, callback) {
  // ...省略参数校验
  if (param instanceof Array) {
    for (var i = 0; i < param.length; i++) {
      callback(param[i], i, param);
    }
  } else if (Object.prototype.toString.call(param) === '[object Object]') {
    for (var val in param) {
      callback(param[val], val, param);
    }
  } else {
    throw new TypeError('each 参数错误!');
  }
}
复制代码

能够看出 each 能够遍历对象或数组,回调函数接受三个参数:

@param {Any} v 当前遍历项
@param {String|Number} k 当前遍历的索引或键
@param {Object|Array} o 当前遍历的对象或者数组
复制代码

有了这个基础函数,咱们能够开始实现咱们的 reduce 函数了:

/** * 迭代数组、类数组对象或对象,返回一个累计值 * @param {Object|Array} param 要迭代的数组、类数组对象或对象 * @param {Function} callback 对每一项进行操做的回调函数,接收四个参数:acc 累加值、v 当前项、k 当前索引、o 当前迭代对象 * @param {Any} initVal 传入的初始值 */
function reduce(param, callback, initVal) {
  var hasInitVal = initVal !== void 0;
  var acc = hasInitVal ? initVal : param[0];
  each(hasInitVal ? param : Array.prototype.slice.call(param, 1), function(v, k, o) {
    acc = callback(acc, v, k, o);
  });
  return acc;
}
复制代码

能够看到,咱们的 reduce 函数就是在 each 上面封装了一层。根据是否传递了初始值 initVal 来决定遍历的起始项。每次遍历都接受 callback 返回的 acc 值,而后在 reduce 的最后返回 acc 累计值就能够啦!

固然,这部分代码有一个很严重的 bug,致使了咱们的 polyfill 毫无心义,那就是遍历对象时的 for...in。这个语法和在 IE <= 9 环境下存在 bug,会没法得到对象的属性值,这就致使咱们所实现的 reduce 没法在 IE 9 如下遍历对象,可是遍历数组仍是能够的。对于 for...in 的这个 bug,能够参考 underscore 是怎么实现的,这里暂时不研究了~

相关文章
相关标签/搜索