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

2018-10-11 admin

在这里插入图片描述

前言

在 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://segmentfault.com/a/1190000016654033

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-42448.html

文章标题:高阶函数应用 —— 柯里化与反柯里化

相关文章
H5即将迎来黄金时代 轻应用再成行业焦点
鎽樿�侊細浠庣伀閫熻交搴旂敤鑾峰緱鎶曡祫绛変腑涓嶉毦鐪嬪嚭锛孒TML5鍗冲皢杩庢潵榛勯噾鏃朵唬銆傝秺鏉ヨ秺澶氱殑浼佷笟鎴栧垱涓氳€呭紑濮嬫秹瓒矵5锛岃�╄交搴旂敤鍐嶆�℃垚涓鸿�屼笟鐨勭劍鐐癸紝鎺ヤ笅鏉ュ皢鏈夋洿澶欻5寮曟搸浠ュ強鏇村�欻5...
2015-11-12
JavaScript初学者必看“箭头函数”
本文我们介绍箭头 (arrow) 函数的优点。 更简洁的语法 我们先来按常规语法定义函数: function funcName(params) { return params + 2; } funcName(2); &#x2F;&#...
2017-05-26
2015 Web 2.0和AJAX如何做好优化
2015如何让做好web2.0和ajax的优化?JavaScript中文网总结当下提出以下四大优化意见,旨在帮助W前端开发人员有效利用APM解决上述问题。 随着Web应用程序速度与效率快速增长,网站已经成为企业与其客户进行交互的第一途径——...
2015-11-12
关于Ajax应用开发需要注意的事项
接触Ajax,那时候的Ajax支持还不是很好,都要涉及底层,没有现成的框架给你调用。现在把常见的问题列举如下。 [b]1、编码问题[/b] 注意AJAX要取的文件是UTF-8编码的。GB2312编码传回BROWSE后中文会乱码。如果用VBS...
2015-11-11
javascript教程:关于if简写语句优化的方法
UglifyJS是一个对javascript进行压缩和美化的工具,在它的文档说明中,我看到了几种关于if语句优化的方法。尽管我还没使用它去做一些尝试性的测试,但从这里可以看到它的确对js作了美化的工作。也许有人认为if语句就那么简单,能优化...
2015-11-12
2015年将会有大量基于HTML5和JS的WEB应用
随着HTML5的定稿,以及JS的迅速发展,我们有理由相信,在接下来的一年里,将会涌现出大量的WEB应用,网站的表现形式将不再仅仅局限于过去的形式,必将在2015年引来一次重大改革! ...
2015-11-12
HTML5移动应用开发的12大特性
1.离线缓存为HTML5开发移动应用提供了基础 HTML5 Web Storage API可以看做是加强版的cookie,不受数据大小限制,有更好的弹性以及架构,可以将数据写入到本机的ROM中,还可以在关闭浏览器后再次打开时恢复数据,以减少...
2015-11-11
移动端网页设计经验与心得
智能手机发展确实很迅速,像今年,我的大部分工作就都在移动端网页上。 再往前些年,看到的手机版/移动版网页,限制于浏览器与手机性能,2g网络速度等 网页设计无非是蓝、黑、白,界面单调,并且要尽可能的设计简单。 现在情况就大不相同了,软件上we...
2015-12-23
图片优化的那些工具
图片作为页面的一个主要因素,它的大小直接影响了页面的加载速度,这一点在移动端尤显突出。 怎么让图片的大小更小?除了选择合适的格式(jpeg、gif、png),我们还可以利用网上的应用(如smushit、tinypng、imagemin、im...
2016-01-13
js监听input输入框值的实时变化实例
1、在元素上同时绑定 oninput 和onporpertychanger事件 例: &lt;script type=&quot;text&#x2F;JavaScript&quot;&gt; function aa(e){alert(&qu...
2017-02-16
回到顶部