缘起
js中Array.prototype.reduce方法在业务编码中很少用到,但是面试题中经常被用到,比如实现一个reduce方法、数组去重、实现compose函数、数组扁平化等等。为了能够更好的了解reduce功能和使用场景,准备重新学习reduce的定义和实现。
定义
reduce
方法对数组中的每个元素执行一个reducer函数(回调函数), 最终汇总结果,返回一个值。
reduce使用方法(语法):
Array.prototype.reduce(reducer(acc, cur[, idx[, src]])[, initValue])
接收两个参数:reducer方法,initValue
其中reducer函数接收4个参数:
Accumulator (acc) (累计器)
Current Value (cur) (当前值)
Current Index (idx) (当前索引)
Source Array (src) (源数组)
reducer方法的返回值返回给acc(累加器),作为下一次迭代的入参,直到迭代结束作为结果返回。
另外一个重要的参数:initValue, 作为累加器初始值,如果使用时没有initValue,则累加器初始值就是数组中的第一个元素;换个角度,initValue影响迭代的次数,如果initValue有值,则迭代次数为数组的长度,如果没有值,迭代次数比数组长度少1。可以看一个简单的例子:
var array = [1, 3, 5, 7, 9]var sum = array.reduce((acc, value, i) =>{ console.log('第' + i + '次迭代', 'acc值:' + acc, '当前值:' + value) return (acc + value)})console.log(sum)var sum1 = array.reduce((acc, value, i) => { console.log('第' + i + '次迭代', 'acc值:' + acc, '当前值:' + value) return (acc + value)}, 10)console.log(sum1)
理解这一点,对实现reduce很关键。
实现
reduce实现的版本很多,我们这边按照最简方式来实现,具体代码如下:
function reduce(array, callback, initValue) { const { length } = array; if (!length) return let startIndex = 1 let acc = array[0] if (initValue) { acc = initValue startIndex = 0 } for(let i = startIndex; i < length; i++) { const value = array[i] acc = callback(acc, value, i, array) } return acc}
MDN参考文档里面有pollify版本
// 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.reduceif (!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; // 如果疑惑 参考 https://stackoverflow.com/questions/1822350/what-is-the-javascript-operator-and-how-do-you-use-it // 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; } });}
应用
数组求和/求积
var array = [1, 2, 3, 4]var sum = array.reduce((acc, value) => (acc + value))
数组去重
除了利用Set去重,也可以使用reduce实现【比较常见面试题】
var array = [1, 2, 2, 4, 3, 3, 5]var result = array.reduce((acc, value) => {if(acc.indexOf(value) === -1) { acc.push(value)}return acc;}, [])
数组扁平化
简单场景:二维转一维
var array = [1, [1, 3], 4, [5, 6, 7]]var flatten = function(arr) { return array.reduce((acc, value) => { acc = acc.concat(value) return acc}, [])}flatten(array)// [1, 1, 3, 4, 5, 6, 7]
复杂一点:多维转一维,需要用到递归知识
var array = [1, [1, [3]], 4, [5, [6, 7]]]var flatten = function(arr) { return arr.reduce((acc, value) => { return acc.concat(Array.isArray(value) ? flatten(value) : value) }, [])}flatten(array)
promise链式操作
按照顺序执行
function p1(num) { return new Promise((resolve, reject) => { resolve(num + 1) })}function p2(num) { return new Promise((resolve, reject) => { resolve(num * 3) })}function p3(num) { return new Promise((resolve, reject) => { resolve(num * 2) })}var array = [p1, p2, p3]var promiseChian= function(array, initValue ) { return array.reduce((acc, item) => { return acc.then(item) }, Promise.resolve(initValue))}promiseChian(array, 5).then(res => {console.log(res)})// 36
compose函数(组合函数)
入门场景
function compose(f, g) { return function(x) { return f(g(x)) }}// demovar test = 2function add(x) { return x + 10}function multiply(x) { return x * 2}compose(add, multiply)(test)// 14
compose函数:调用时,组合函数按顺序从右向左执行,右边函数调用后,返回的结果,作为左边函数的参数传入。
function f1(arg) { console.log(arg, 'f1') return arg + 1}function f2(arg) { console.log(arg, 'f2') return arg + 2}function f3(arg) { console.log(arg, 'f3') return arg + 3}function f4(arg) { console.log(arg, 'f4') return arg + 4}// 这样看起来有些懵 且不易维护f4(f3(f2(f1(1)))) // 输出:11// 可以利用compose函数来实现这个功能compose(f1, f2, f3, f4)(1)
下面使用 reduce
来实现compose
function compose() { var fns = Array.prototype.slice.call(arguments) return (arg) => { if(fns.length === 0) { return () => {} } else if(fns.length === 1){ return fns[0] } return fns.reverse().reduce((acc, value) => { return value(acc) // 每次当前函数处理之前函数的结果,其中acc初始值就是入参 }, arg) }}
还有一种面试版本(1行代码)
var compose = (...fns)=> fns.reduce((a, b) => (...arg) => a(b(...arg)))
根据简单的例子可以分析一下这个方法:compose(f4, f3, f2, f1)('hello', 'world')
reduce第一次执行:返回值为 函数 (...arg)=> f4(f3(...arg)), 作为下一次 执行 a 的值
第二次执行:返回值为 函数 (...arg)=> f4(f3(f2(...arg))), 作为下一次执行 a的值
第三次执行:返回值为 函数 (...arg) => f4(f3(f2(f1(...arg))))
compose函数返回值是一个函数,compose(f4, f3, f2, f1)('hello', 'world')实际执行的方法就是
f4(f3(f2(f1('hello', 'world'))))
计数器
统计数组中各个元素出现的次数,返回结果对象
var countItem = function(arr) { return arr.reduce((acc, value) => { if(!acc[value]) { acc[value] = 1 } else { acc[value]++ } return acc }, {})}var array = ['a', 'b', 'c', 'h', 'o', 'a', 'b', 'c', 'a']countItem(array)// {a: 3, b: 2, c: 2, h: 1, o: 1}
参考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
https://github.com/reduxjs/redux/blob/v3.7.2/src/compose.js (redux中compose实现方法)