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 是怎么實現的,這里暫時不研究了~