reduce介绍

image.png
reducer 函数接收4个参数:

  1. Accumulator (acc) (累计器)
  2. Current Value (cur) (当前值)
  3. Current Index (idx) (当前索引)
  4. Source Array (src) (源数组)

您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。

runPromiseInSequence

runPromiseInSequence 方法将会被一个每一项都返回一个 Promise 的数组调用,并且依次执行数组中的每一个 Promise,请读者仔细体会。如果觉得晦涩,可以参考示例:
`const f1 = () => new Promise((resolve, reject) => {
setTimeout(() => {
console.log(‘p1 running’)
resolve(1)
}, 1000)
})

const f2 = () => new Promise((resolve, reject) => {
setTimeout(() => {
console.log(‘p2 running’)
resolve(2)
}, 1000)
})

const array = [f1, f2]

const runPromiseInSequence = (array, value) => array.reduce(<br /> (promiseChain, currentFunction) => promiseChain.then(currentFunction),<br /> Promise.resolve(value)<br />)

runPromiseInSequence(array, ‘init’)`
执行结果如下图:

reduce - 图2

reduce 实现 pipe

reduce 的另外一个典型应用可以参考函数式方法 pipe 的实现:pipe(f, g, h) 是一个 curry 化函数,它返回一个新的函数,这个新的函数将会完成 (...args) => h(g(f(...args))) 的调用。即 pipe 方法返回的函数会接收一个参数,这个参数传递给 pipe 方法第一个参数,以供其调用。
const pipe = (...functions) => input => functions.reduce(<br /> (acc, fn) => fn(acc),<br /> input<br />)
仔细体会 runPromiseInSequencepipe 这两个方法,它们都是 reduce 应用的典型场景。

实现一个 reduce

那么我们该如何实现一个 reduce 呢?参考来自 MDN 的 polyfill:
`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. var o = Object(this)
  2. var len = o.length >>> 0
  3. var k = 0<br /> var value
  4. if (arguments.length >= 2) {<br /> value = arguments[1]<br /> } else {<br /> while (k < len && !(k in o)) {<br /> k++<br /> }
  5. if (k >= len) {<br /> throw new TypeError( 'Reduce of empty array ' +<br /> 'with no initial value' )<br /> }<br /> value = o[k++]<br /> }
  6. while (k < len) {<br /> if (k in o) {<br /> value = callback(value, o[k], k, o)<br /> }
  7. k++<br /> }
  8. return value<br /> }<br /> })<br />}`<br />上述代码中使用了 `value` 作为初始值,并通过 `while` 循环,依次累加计算出 `value` 结果并输出。但是相比 MDN 上述实现,我个人更喜欢的实现方案是:<br />`Array.prototype.reduce = Array.prototype.reduce || function(func, initialValue) {<br /> var arr = this<br /> var base = typeof initialValue === 'undefined' ? arr[0] : initialValue<br /> var startPoint = typeof initialValue === 'undefined' ? 1 : 0<br /> arr.slice(startPoint)<br /> .forEach(function(val, index) {<br /> base = func(base, val, index + startPoint, arr)<br /> })<br /> return base<br />}`<br />核心原理就是使用 `forEach` 来代替 `while` 实现结果的累加,它们本质上是相同的。<br />我也同样看了下 ES5-shim 里的 pollyfill,跟上述思路完全一致。唯一的区别在于:我用了 `forEach` 迭代而 ES5-shim 使用的是简单的 `for` 循环。实际上,如果「杠精」一些,我们会指出数组的 `forEach` 方法也是 ES5 新增的。因此,用 ES5 的一个 API(`forEach`),去实现另外一个 ES5 的 API(`reduce`),这并没什么实际意义——这里的 pollyfill 就是在不兼容 ES5 的情况下,模拟的降级方案。此处不多做追究,因为根本目的还是希望读者对 `reduce` 有一个全面透彻的了解。

通过 Koa only 模块源码认识 reduce

通过了解并实现 reduce 方法,我们对它已经有了比较深入的认识。最后,再来看一个 reduce 使用示例——通过 Koa 源码的 only 模块,加深印象:
var o = {<br /> a: 'a',<br /> b: 'b',<br /> c: 'c'<br />}<br />only(o, ['a','b']) // {a: 'a', b: 'b'}
该方法返回一个经过指定筛选属性的新对象。
only 模块实现:
var only = function(obj, keys){<br /> obj = obj || {}<br /> if ('string' == typeof keys) keys = keys.split(/ +/)<br /> return keys.reduce(function(ret, key) {<br /> if (null == obj[key]) return ret<br /> ret[key] = obj[key]<br /> return ret<br /> }, {})<br />}
小小的 reduce 及其衍生场景有很多值得我们玩味、探究的地方。举一反三,活学活用是技术进阶的关键。

compose 实现的几种方案

函数式理念——这一古老的概念如今在前端领域「遍地开花」。函数式很多思想都值得借鉴,其中一个细节:compose 因为其巧妙的设计而被广泛运用。对于它的实现,从面向过程式到函数式实现,风格迥异,值得我们探究。在面试当中,也经常有面试官要求实现 compose 方法,我们先看什么是 compose
compose 其实和前面提到的 pipe 一样,就是执行一连串不定长度的任务(方法),比如:
let funcs = [fn1, fn2, fn3, fn4]<br />let composeFunc = compose(...funcs)
执行:
composeFunc(args)
就相当于:
fn1(fn2(fn3(fn4(args))))
总结一下 compose 方法的关键点:

  • compose 的参数是函数数组,返回的也是一个函数
  • compose 的参数是任意长度的,所有的参数都是函数,执行方向是自右向左的,因此初始函数一定放到参数的最右面
  • compose 执行后返回的函数可以接收参数,这个参数将作为初始函数的参数,所以初始函数的参数是多元的,初始函数的返回结果将作为下一个函数的参数,以此类推。因此除了初始函数之外,其他函数的接收值是一元的

我们发现,实际上,composepipe 的差别只在于调用顺序的不同:
`// compose
fn1(fn2(fn3(fn4(args))))

// pipe
fn4(fn3(fn2(fn1(args))))<br />即然跟我们先前实现的pipe方法如出一辙,那么还有什么好深入分析的呢?请继续阅读,看看还能玩出什么花儿来。<br />compose最简单的实现是面向过程的:<br />const compose = function(…args) {
let length = args.length
let count = length - 1
let result
return function f1 (…arg1) {
result = args[count].apply(this, arg1)
if (count <= 0) {
count = length - 1
return result
}
count—
return f1.call(null, result)
}
}<br />这里的关键是用到了**闭包**,使用闭包变量储存结果result和函数数组长度以及遍历索引,并利用递归思想,进行结果的累加计算。整体实现符合正常的面向过程思维,不难理解。<br />聪明的读者可能也会意识到,利用上文所讲的reduce方法,应该能更**函数式**地解决问题:<br />const reduceFunc = (f, g) => (…arg) => g.call(this, f.apply(this, arg))
const compose = (…args) => args.reverse().reduce(reduceFunc, args.shift())<br />通过前面的学习,结合callapply方法,这样的实现并不难理解。<br />**我们继续开拓思路,「既然涉及串联和流程控制」,那么还可以使用 Promise 实现:**<br />const compose = (…args) => {
let init = args.pop()
return (…arg) =>
args.reverse().reduce((sequence, func) =>
sequence.then(result => func.call(null, result))
, Promise.resolve(init.apply(null, arg)))
}<br />这种实现利用了 Promise 特性:首先通过Promise.resolve(init.apply(null, arg))启动逻辑,启动一个resolve值为最后一个函数接收参数后的返回值,依次执行函数。因为promise.then()仍然返回一个 Promise 类型值,所以reduce完全可以按照 Promise 实例执行下去。<br />既然能够使用 Promise 实现,那么 **generator** 当然应该也可以实现。这里给大家留一个思考题,感兴趣的读者可以尝试,欢迎在评论区或读者群讨论。<br />最后,我们再看下社区上著名的 lodash 和 Redux 的实现。<br />**lodash 版本**<br />// lodash 版本
var compose = function(funcs) {
var length = funcs.length
var index = length
while (index—) {
if (typeof funcs[index] !== ‘function’) {
throw new TypeError(‘Expected a function’);
}
}
return function(…args) {
var index = 0
var result = length ? funcs.reverse()[index].apply(this, args) : args[0]
while (++index < length) {
result = funcs[index].call(this, result)
}
return result
}
}<br />lodash 版本更像我们的第一种实现方式,理解起来也更容易。<br />**Redux 版本**<br />// Redux 版本
function compose(…funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (…args) => a(b(…args)))
}<br />总之,还是充分利用了数组的reduce` 方法。
函数式概念确实有些抽象,需要开发者仔细琢磨,并动手调试。一旦顿悟,必然会感受到其中的优雅和简洁。