高阶函数应用 —— 柯里化与反柯里化

在这里插入图片描述


阅读原文


前言

在 JavaScript 中,柯里化和反柯里化是高阶函数的一种应用,在这以前咱们应该清楚什么是高阶函数,通俗的说,函数能够做为参数传递到函数中,这个做为参数的函数叫回调函数,而拥有这个参数的函数就是高阶函数,回调函数在高阶函数中调用并传递相应的参数,在高阶函数执行时,因为回调函数的内部逻辑不一样,高阶函数的执行结果也不一样,很是灵活,也被叫作函数式编程。正则表达式


柯里化

在 JavaScript 中,函数柯里化是函数式编程的重要思想,也是高阶函数中一个重要的应用,其含义是给函数分步传递参数,每次传递部分参数,并返回一个更具体的函数接收剩下的参数,这中间可嵌套多层这样的接收部分参数的函数,直至返回最后结果。编程

一、最基本的柯里化拆分

// 柯里化拆分
// 原函数
function add(a, b, c) {
    return a + b + c;
}

// 柯里化函数
function addCurrying(a) {
    return function (b) {
        return function (c) {
            return a + b + c;
        }
    }
}

// 调用原函数
add(1, 2, 3); // 6

// 调用柯里化函数
addCurrying(1)(2)(3) // 6

被柯里化的函数 addCurrying 每次的返回值都为一个函数,并使用下一个参数做为形参,直到三个参数都被传入后,返回的最后一个函数内部执行求和操做,实际上是充分的利用了闭包的特性来实现的。数组

二、柯里化通用式

上面的柯里化函数没涉及到高阶函数,也不具有通用性,没法转换形参个数任意或未知的函数,咱们接下来封装一个通用的柯里化转换函数,能够将任意函数转换成柯里化。闭包

// 柯里化通用式 ES5
function currying(func, args) {
    // 形参个数
    var arity = func.length;
    // 上一次传入的参数
    var args = args || [];

    return function () {
        // 将参数转化为数组
        var _args = [].slice.call(arguments);

        // 将上次的参数与当前参数进行组合并修正传参顺序
        Array.prototype.unshift.apply(_args, args);

        // 若是参数不够,返回闭包函数继续收集参数
        if(_args.length < arity) {
            return currying.call(null, func, _args);
        }

        // 参数够了则直接执行被转化的函数
        return func.apply(null, _args);
    }
}

上面主要使用的是 ES5 的语法来实现,大量的使用了 callapply,下面咱们经过 ES6 的方式实现功能彻底相同的柯里化转换通用式。app

// 柯里化通用式 ES6
function currying(func, args = []) {
    let arity = func.length;

    return function (..._args) {
        _args.unshift(...args);

        if(_args.length < arity) {
            return currying(func, _args);
        }

        return func(..._args);
    }
}

函数 currying 算是比较高级的转换柯里化的通用式,能够随意拆分参数,假设一个被转换的函数有多个形参,咱们能够在任意环节传入任意个数的参数进行拆分,举一个例子,假如 5 个参数,第一次能够传入 2 个,第二次能够传入 1 个, 第三次能够传入剩下的,也有其余的多种传参和拆分方案,由于在 currying 内部收集参数的同时按照被转换函数的形参顺序进行了更正。函数式编程

柯里化的一个很大的好处是能够帮助咱们基于一个被转换函数,经过对参数的拆分实现不一样功能的函数,以下面的例子。函数

// 柯里化通用式应用 —— 普通函数
// 被转换函数,用于检测传入的字符串是否符合正则表达式
function checkFun(reg, str) {
    return reg.test(str);
}

// 转换柯里化
const check = currying(checkFun);

// 产生新的功能函数
const checkPhone = check(/^1[34578]\d{9}$/);
const checkEmail = check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

上面的例子根据一个被转换的函数经过转换变成柯里化函数,并用 check 变量接收,之后每次调用 check 传递不一样的正则就会产生一个检测不一样类型字符串的功能函数。this

这种使用方式一样适用于被转换函数是高阶函数的状况,好比下面的例子。spa

// 柯里化通用式应用 —— 高阶函数
// 被转换函数,按照传入的回调函数对传入的数组进行映射
function mapFun(func, array) {
    return array.map(func);
}

// 转换柯里化
const getNewArray = currying(mapFun);

// 产生新的功能函数
const createPercentArr = getNewArray(item => `${item * 100}%`);
const createDoubleArr = getNewArray(item => item * 2);

// 使用新的功能函数
let arr = [1, 2, 3, 4, 5];
let percentArr = createPercentArr(arr); // ['100%', '200%', '300%', '400%', '500%',]
let doubleArr = createDoubleArr(arr); // [2, 4, 6, 8, 10]

三、柯里化与 bind

bind 方法是常用的一个方法,它的做用是帮咱们将调用 bind 函数内部的上下文对象 this 替换成咱们传递的第一个参数,并将后面其余的参数做为调用 bind 函数的参数。prototype

// bind 方法原理模拟
// bind 方法的模拟
Function.prototype.bind = function (context) {
    var self = this;
    var args = [].slice.call(arguments, 1);

    return function () {
        return self.apply(context, args);
    }
}

经过上面代码能够看出,其实 bind 方法就是一个柯里化转换函数,将调用 bind 方法的函数进行转换,即经过闭包返回一个柯里化函数,执行该柯里化函数的时候,借用 apply 将调用 bind 的函数的执行上下文转换成了 context 并执行,只是这个转换函数没有那么复杂,没有进行参数拆分,而是函数在调用的时候传入了全部的参数。


反柯里化

反柯里化的思想与柯里化正好相反,若是说柯里化的过程是将函数拆分红功能更具体化的函数,那反柯里化的做用则在于扩大函数的适用性,使原本做为特定对象所拥有的功能函数能够被任意对象所使用。

一、反柯里化通用式

反柯里化通用式的参数为一个但愿能够被其余对象调用的方法或函数,经过调用通用式返回一个函数,这个函数的第一个参数为要执行方法的对象,后面的参数为执行这个方法时须要传递的参数。

// 反柯里化通用式 ES5
function uncurring(fn) {
    return function () {
        // 取出要执行 fn 方法的对象,同时从 arguments 中删除
        var obj = [].shift.call(arguments);
        return fn.apply(obj, arguments);
    }
}
// 反柯里化通用式 ES6
function uncurring(fn) {
    return function (...args) {
        return fn.call(...args);
    }
}

下面咱们经过一个例子来感觉一下反柯里化的应用。

// 反柯里化通用式应用
// 构造函数 F
function F() {}

// 拼接属性值的方法
F.prototype.concatProps = function () {
    let args = Array.from(arguments);
    return args.reduce((prev, next) => `${this[prev]}&${this[next]}`);
}

// 使用 concatProps 的对象
let obj = {
    name: "Panda",
    age: 16
};

// 使用反柯里化进行转化
const concatProps = uncurring(F.prototype.concatProps);

concatProps(obj, "name", "age"); // Panda&16

反柯里化还有另一个应用,用来代替直接使用 callapply,好比检测数据类型的 Object.prototype.toString 等方法,以往咱们使用时是在这个方法后面直接调用 call 更改上下文并传参,若是项目中多处须要对不一样的数据类型进行验证是很麻的,常规的解决方案是封装成一个检测数据类型的模块。

// 检测数据类型常规方案
function checkType(val) {
    return Object.prototype.toString.call(val);
}

若是须要这样封装的功能不少就麻烦了,代码量也会随之增大,其实咱们也可使用另外一种解决方案,就是利用反柯里化通用式将这个函数转换并将返回的函数用变量接收,这样咱们只须要封装一个 uncurring 通用式就能够了。

// 反柯里化建立检测类型函数
const checkType = uncurring(Object.prototype.toString);

checkType(1); // [object Number]
checkType("hello"); // [object String]
checkType(true); // [object Boolean]

二、经过函数调用生成反柯里化函数

在 JavaScript 咱们常用面向对象的编程方式,在两个类或构造函数之间创建联系实现继承,若是咱们对继承的需求仅仅是但愿一个构造函数的实例可以使用另外一个构造函数原型上的方法,那进行繁琐的继承很浪费,简单的继承父子类的关系又不那么的优雅,还不如之间不存在联系。

// 将反柯里化方法扩展到函数原型
Function.prototype.uncurring = function () {
    var self = this;
    return function () {
        return Function.prototype.call.apply(self, arguments);
    }
}

以前的问题经过上面给函数扩展的 uncurring 方法彻底获得了解决,好比下面的例子。

// 函数应用反柯里化原型方法
// 构造函数
function F() {}

F.prototype.sayHi = function () {
    return "I'm " + this.name + ", " + this.age + " years old.";
}

// 但愿 sayHi 方法被任何对象使用
sayHi = F.prototype.sayHi.uncurring();

sayHi({ name: "Panda", age: 20}); // I'm Panda, 20 years old.

Function 的原型对象上扩展的 uncurring 中,难点是理解 Function.prototype.call.apply,咱们知道在 call 的源码逻辑中 this 指的是调用它的函数,在 call 内部用第一个参数替换了这个函数中的 this,其他做为形参执行了函数。

而在 Function.prototype.call.applyapply 的第一个参数更换了 call 中的 this,这个用于更换 this 的就是例子中调用 uncurring 的方法 F.prototype.sayHi,因此等同于 F.prototype.sayHi.callarguments 内的参数会传入 call 中,而 arguments 的第一项正是用于修改 F.prototype.sayHithis 的对象。


总结

看到这里你应该对柯里化和反柯里化有了一个初步的认识了,但要熟练的运用在开发中,还须要咱们更深刻的去了解它们内在的含义。

相关文章
相关标签/搜索