reduce介绍
reducer 函数接收4个参数:
- Accumulator (acc) (累计器)
- Current Value (cur) (当前值)
- Current Index (idx) (当前索引)
- 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
实现 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 />)
仔细体会 runPromiseInSequence
和 pipe
这两个方法,它们都是 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’)
}
var o = Object(this)
var len = o.length >>> 0
var k = 0<br /> var value
if (arguments.length >= 2) {<br /> value = arguments[1]<br /> } else {<br /> while (k < len && !(k in o)) {<br /> k++<br /> }
if (k >= len) {<br /> throw new TypeError( 'Reduce of empty array ' +<br /> 'with no initial value' )<br /> }<br /> value = o[k++]<br /> }
while (k < len) {<br /> if (k in o) {<br /> value = callback(value, o[k], k, o)<br /> }
k++<br /> }
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
执行后返回的函数可以接收参数,这个参数将作为初始函数的参数,所以初始函数的参数是多元的,初始函数的返回结果将作为下一个函数的参数,以此类推。因此除了初始函数之外,其他函数的接收值是一元的
我们发现,实际上,compose
和 pipe
的差别只在于调用顺序的不同:
`// 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 />通过前面的学习,结合
call、
apply方法,这样的实现并不难理解。<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` 方法。
函数式概念确实有些抽象,需要开发者仔细琢磨,并动手调试。一旦顿悟,必然会感受到其中的优雅和简洁。