资讯专栏INFORMATION COLUMN

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

wyk1184 / 1892人阅读

摘要:柯里化通用式上面的柯里化函数没涉及到高阶函数,也不具备通用性,无法转换形参个数任意或未知的函数,我们接下来封装一个通用的柯里化转换函数,可以将任意函数转换成柯里化。


阅读原文


前言

在 JavaScript 中,柯里化和反柯里化是高阶函数的一种应用,在这之前我们应该清楚什么是高阶函数,通俗的说,函数可以作为参数传递到函数中,这个作为参数的函数叫回调函数,而拥有这个参数的函数就是高阶函数,回调函数在高阶函数中调用并传递相应的参数,在高阶函数执行时,由于回调函数的内部逻辑不同,高阶函数的执行结果也不同,非常灵活,也被叫做函数式编程。


柯里化

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

1、最基本的柯里化拆分
// 柯里化拆分
// 原函数
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 每次的返回值都为一个函数,并使用下一个参数作为形参,直到三个参数都被传入后,返回的最后一个函数内部执行求和操作,其实是充分的利用了闭包的特性来实现的。

2、柯里化通用式

上面的柯里化函数没涉及到高阶函数,也不具备通用性,无法转换形参个数任意或未知的函数,我们接下来封装一个通用的柯里化转换函数,可以将任意函数转换成柯里化。

// 柯里化通用式 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 的方式实现功能完全相同的柯里化转换通用式。

// 柯里化通用式 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 传递不同的正则就会产生一个检测不同类型字符串的功能函数。

这种使用方式同样适用于被转换函数是高阶函数的情况,比如下面的例子。

// 柯里化通用式应用 —— 高阶函数
// 被转换函数,按照传入的回调函数对传入的数组进行映射
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]
3、柯里化与 bind

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

// 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 并执行,只是这个转换函数没有那么复杂,没有进行参数拆分,而是函数在调用的时候传入了所有的参数。


反柯里化

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

1、反柯里化通用式

反柯里化通用式的参数为一个希望可以被其他对象调用的方法或函数,通过调用通用式返回一个函数,这个函数的第一个参数为要执行方法的对象,后面的参数为执行这个方法时需要传递的参数。

// 反柯里化通用式 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]
2、通过函数调用生成反柯里化函数

在 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 的对象。


总结

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


文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98307.html

相关文章

  • 柯里简介

    摘要:与反柯里化什么是柯里化与反柯里化纯函数函数结果只受传入参数影响,参数一定,结果一定。写法柯里化接受一次性传入多个参数调用的函数,也可以传入部分参数调用,最后使它返回一个单一参数的函数去处理,并且返回。 call与apply反柯里化? 什么是柯里化与反柯里化?纯函数函数结果只受传入参数影响,参数一定,结果一定。高阶函数一个函数可以接收另一个函数作为参数,这种函数称为高阶函数。 funct...

    songjz 评论0 收藏0
  • 函数柯里Redux中间件及applyMiddleware源码分析

    摘要:函数的柯里化的基本使用方法和函数绑定是一样的使用一个闭包返回一个函数。先来一段我自己实现的函数高程里面这么评价它们两个的方法也实现了函数的柯里化。使用还是要根据是否需要对象响应来决定。 奇怪,怎么把函数的柯里化和Redux中间件这两个八竿子打不着的东西联系到了一起,如果你和我有同样疑问的话,说明你对Redux中间件的原理根本就不了解,我们先来讲下什么是函数的柯里化?再来讲下Redux的...

    jeyhan 评论0 收藏0
  • 函数式编程了解一下(上)

    摘要:一直以来没有对函数式编程有一个全面的学习和使用,或者说没有一个深刻的思考。是不是轻松了其实函数式编程主张的就是以抽象的方式创建函数。后面咱们在系统性的学习下函数式编程。 一直以来没有对函数式编程有一个全面的学习和使用,或者说没有一个深刻的思考。最近看到一些博客文章,突然觉得函数式编程还是蛮有意思的。看了些书和文章。这里记载下感悟和收获。 欢迎团队姜某人多多指点@姜少。 由于博客秉持着简...

    int64 评论0 收藏0
  • 掌握JavaScript函数柯里

    摘要:原文链接和都支持函数的柯里化函数的柯里化还与的函数编程有很大的联系如果你感兴趣的话可以在这些方面多下功夫了解相信收获一定很多看本篇文章需要知道的一些知识点函数部分的闭包高阶函数不完全函数文章后面有对这些知识的简单解释大家可以看看什么是柯里化 原文链接 Haskell和scala都支持函数的柯里化,JavaScript函数的柯里化还与JavaScript的函数编程有很大的联系,如果你感兴...

    DTeam 评论0 收藏0
  • 关于js中的柯里(Currying)与反柯里(Uncurrying)

    摘要:今天了解到一个新名词柯里化,研究一番后总结如下一柯里化定义把接受多个参数的函数变换成接受一个单一参数最初函数的第一个参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。如果使用反柯里化,则可以这样写震惊某前端只会,竟月入百万。。。 今天了解到一个新名词:柯里化,研究一番后总结如下: 一· 柯里化 定义 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并...

    Rocko 评论0 收藏0

发表评论

0条评论

wyk1184

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<