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

参考https://www.pandashen.com/2018/06/23/20180623084025/

前言

在 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);
    }
}

//test
currying(add, [1,2,3])()
currying(add, [2,3])(1)
currying(add, [3])(1)(2)
currying(add, [])(1)(2)(3)
currying(add)(1)(2)(3)

上面主要使用的是 ES5 的语法来实现,大量的使用了 call 和 apply,下面我们通过 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

反柯里化还有另外一个应用,用来代替直接使用 call 和 apply,比如检测数据类型的 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.apply 中 apply 的第一个参数更换了 call 中的 this,这个用于更换 this 的就是例子中调用 uncurring 的方法 F.prototype.sayHi,所以等同于 F.prototype.sayHi.call,arguments 内的参数会传入 call 中,而 arguments 的第一项正是用于修改 F.prototype.sayHi 中 this 的对象。

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容

  • 柯里化是函数的一个高级应用,想要理解它并不简单。因此我一直在思考应该如何更加表达才能让大家理解起来更加容易。 通过...
    这波能反杀阅读 68,941评论 117 318
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,560评论 0 5
  • 昨天在超市买完东西排队时听到不远处一个妈妈训斥自己的孩子:“今天什么都不给你!不行!就是不买!”我没听到孩子的声音...
    梅子Mey阅读 1,567评论 1 3
  • 你有没有这样过,或者有没有想这样过,人生就是这样,体验人生百态,品味酸甜苦辣,人生中的每一次经历都是收获,都是回赠...
    醉田园阅读 209评论 0 0
  • 杨贵云焦点43期坚持原创分享第171天平顶山 大部分的孩子们明天就要开始期末考试了,那么作为家长的考时应该怎么做呢...
    舒静心阅读 86评论 0 0