Array.prototype.reduce 的理解與實現

Russ_Zhong發表於2018-12-16

Array.prototype.reduce 是 JavaScript 中比較實用的一個函式,但是很多人都沒有使用過它,因為 reduce 能做的事情其實 forEach 或者 map 函式也能做,而且比 reduce 好理解。但是 reduce 函式還是值得去了解的。

reduce 函式可以對一個陣列進行遍歷,然後返回一個累計值,它使用起來比較靈活,下面瞭解一下它的用法。

reduce 接受兩個引數,第二個引數可選:

@param {Function} callback 迭代陣列時,求累計值的回撥函式
@param {Any} initVal 初始值,可選
複製程式碼

其中,callback 函式可以接受四個引數:

@param {Any} acc 累計值
@param {Any} val 當前遍歷的值
@param {Number} key 當前遍歷值的索引
@param {Array} arr 當前遍歷的陣列
複製程式碼

callback 接受這四個引數,經過處理後返回新的累計值,而這個累計值會作為新的 acc 傳遞給下一個 callback 處理。直到處理完所有的陣列項。得到一個最終的累計值。

reduce 接受的第二個引數是一個初始值,它是可選的。如果我們傳遞了初始值,那麼它會作為 acc 傳遞給第一個 callback,此時 callback 的第二個引數 val 是陣列的第一項;如果我們沒有傳遞初始值給 reduce,那麼陣列的第一項會作為累計值傳遞給 callback,陣列的第二項會作為當前項傳遞給 callback。

示例:

對陣列求和:

let arr = [1, 2, 3];
let res = arr.reduce((acc, v) => acc + v);
console.log(res); // 6
複製程式碼

如果我們傳遞一個初始值:

let arr = [1, 2, 3];
let res = arr.reduce((acc, v) => acc + v, 94);
console.log(res); // 100
複製程式碼

利用 reduce 求和比 forEach 更加簡單,程式碼也更加優雅,只需要清楚 callback 接受哪些引數,代表什麼含義就可以了。

我們還可以利用 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 是怎麼實現的,這裡暫時不研究了~

相關文章