reduce与compose

650 阅读4分钟

前言

最近刚上手redux,redux的实现原理是基于compose组合函数。redux中实现compose函数的方式也充分利用了reduce。函数式的概念对于我来说也比较晦涩难懂。

reduce MDN

reduce是es5出的一个数组操作的方法。MDN中对该方法的描述是(reduce方法通过一个给定的执行函数,对数组每一项取值从左到右累加,最终产生一个结果)。

简要介绍一下reduce的参数

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

【callback】是一个回调函数

  • accumulator上一次的累加值。
  • currentValue数组遍历中正在处理的函数
  • index 对应的索引 (如果提供了initialValue索引号为0,否则为1)
  • array为方法调用的数组 【initialValue】是callback的第一个参数的初始值。 没有提供将数组的第一个元素作为callback第一个参数的初始值

reduce实现原理

MDN上也有相关的polyfill代码。下面为了让代码可以运行,我删除了原有的注释并稍微做了一点改动。

let obj = [10, 11, 13];

Object.defineProperty(obj, '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;
        // 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++;
            }
            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) {
            if (k in o) {
                value = callback(value, o[k], k, o);
            }
            // d. Increase k by 1.
            k++;
        }
        // 9. Return accumulator.
        return value;
    }
});
console.log(obj.reduce((cur, currItem) => {
    return cur + currItem
}))

上面我定义了一个数组[1,2,3]执行自定义reduce函数,每一步的执行过程如下。

- 第一种情况:没有传初始值会将数组的第一个值作为初始值。

执行次数kvalue传给回调函数的值
初始状态0undefined
第一次110callback(10, 11, 1, [10,11,12])
第二次221callback(21, 12, 2, [10,11,12])
第三次334callback(34, undefined, 3, [10,11,12])
  • 第二种情况传了初始值 传了初始值的时候只是在初始状态这一步将函数的第二个参数作为初始值
if (arguments.length >= 2) {
   value = arguments[1];
}

reduce高级应用

(1)顺序执行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 runPromiseAarrayFun = (array, value) => {
    return array.reduce((promisefn, currentFunction) => promisefn.then(currentFunction), Promise.resolve(value))
}
runPromiseAarrayFun(array, 'init')
  • 输出
p1 running
p2 running
  • 说明:实际上es6中已经有了异步函数“同步执行的方案”。比如async await也可以实现promise同步执行。但是async await只能执行固定个数的异步函数。当异步函数的个数不确定时使用async await无法做到。

(2)pipe实现原理

pipe是一个柯里化函数,接收一个参数作为初始值,其原理是解决g(f(m(...arg)))嵌套调用时嵌套的深度不确定问题。

  • 例子:来源于MDN
// Building-blocks to use for composition
const double = x => x + x;
const triple = x => 3 * x;
const quadruple = x => 4 * x;
// Function composition enabling pipe functionality
const pipe = (...functions) => input => functions.reduce(
    (acc, fn) => {
        debugger
        return fn(acc)
    },
    input
);
const multiply24 = pipe(double, triple, quadruple);
multiply24(10); // 240

执行multiply24(10)的时候,参数10会依次执行上面的三个函数,但是下一次函数的参数值为上一次函数执行的结果。这一特性和reduce的原理相似。

  • 执行过程
执行次数传给下一次函数的值等价
初始状态10
第一次10double(10)double(10)
第二次20triple(20)triple(double(10))
第三次60quadruple(60)quadruple(triple(double(10)))

compose

compose也被称为组合函数。他和pipe的原理相似。主要用于执行一连串不定长的任务。

  • pipe的执行顺序
let funarrs = [f1,f2,f3,f4];
pipe(...funarrs);
相当于
f4(f3(f2(f1(...args))));
  • pipe的执行顺序
let funarrs = [f1,f2,f3,f4];
pipe(...funarrs);
相当于
f1(f2(f3(f4(...args))));

递归方式实现

const compose = function (...args) {
    let length = args.length;
    let count = length - 1;
    let result
    return function f1(...arg1) {
        debugger
        result = args[count].apply(this, arg1);
        if (count <= 0) {
            count = length - 1;
            return result;
        }
        count--;
        console.log('countttttttttttttttt')
        return f1.call(null, result)
    }
}
function f1() {
    console.log("f1")
    return 1;
}
function f2() {
    console.log("f2")
    return 2;
}
function f3() {
    console.log("f3");
    return 3;
}
function f4() {
    console.log("f4")
    return 4;
}
let funcs = [f1, f2, f3, f4];
let composeFunc = compose(...funcs)
执行次数递归是否结束count值执行函数arg
第一次3f4
第二次2f34
第三次1f23
第四次0f1 f1.apply(this, arg1)3

这里使用到了闭包,执行的时候缓存了compose函数中的count参数。每次的执行逻辑就是将上一次返回值作为这次执行的参数,可以过程可以利用递归,递归结束的条件是当count==0的时候,跳出递归。直接执行f1函数。

reduce实现

const compose = (...args) => {
    return args.reverse().reduce((f, g) => {
        return (...arg) => {
            return g.call(this, f.apply(this, arg))
        }
    }, args.shift())
}
function f1(x) {
    return x + 1;
}
function f2(x) {
    return x + 2;
}
function f3(x) {
    return x + 3;
}
function f4(x) {
    return x + 4;
}
let funcs = [f1, f2, f3, f4]
console.log(compose(...funcs)(0))
  • 第一次初始值为0-->args.shift()==f4--->g.call(this, f.apply(this, arg))<=>f3.call(this, f4.apply(this, 0));
  • 第二次0-->g.call(this, f.apply(this, arg))<=>f2.call(this, f3.apply(this, f3.call(this, f4.apply(this, 0))));
  • 第三次0-->g.call(this, f.apply(this, arg))<=>f1.call(this, f2.apply(this, f2.call(this, f3.apply(this, f4.apply(this, 0)))));
  • 第四次:g.call(this, f.apply(this, arg))<=>f1.call(this, f2.apply(this, f2.call(this, f3.apply(this, f4.apply(this, 0)))));

reduce实现compose的原理就是利用reduce累加特新,将上一次累计的函数值,作为当前执行函数的参数。

参考资料

  1. pipe
  2. compose