资讯专栏INFORMATION COLUMN

邂逅函数柯里化

Kross / 3101人阅读

摘要:柯里化函数的作用函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。指向的是调用方法的一个函数,绑定,延迟执行可见,之后返回的是一个延迟执行的新函数关于性能的备注通常,使用柯里化会有一些开销。

引子

有这样一道题目,实现一个函数,实现如下功能:

var result = sum(1)(2)(3);

console.log(result);//6

这道题目,印象中是一道技术笔试题。结合查到的资料,在这里做一下简单的分析和总结。

一个简单的例子

题目给的还是比较宽的,没多少限制,给了很多自由发挥的空间。

下面我们就一步一步的去实现,一种简单的做法可以是这样的:

function add(a){
    var sum = 0;
        sum += a;
    return function(b){
        sum += b;
        return function(c){
            sum += c;
            return sum;
        }
    }
}

add(1)(2)(3);//6

嗯,没什么问题。

在此基础上,我们再进一步:

如果对调用的次数不加限制,比如 四次,那上面的代码就不行了。
那该怎么办呢?

观察一下,我们可以发现返回的每一个函数执行的逻辑其实都是一样的。
就此我们可以精简下代码,让函数返回后返回自身。

来试一下:

function add(a){

    var sum = 0;
    sum += a; 
    
    return function temp(b) {
        sum += b;
        return temp;
    }
}

add(2)(3)(4)(5);

输出的结果:

//function temp(b) {
//        sum += b;
//        return temp;
//    }

并没有像我们预期的那样输出 14,其实是这样的,每次函数调用后返回的就是一个函数对象,那最后的结果,肯定是一个字符串表示啊。

要修正的话,有两个办法。

判断参数,当没有输入参数时,返回调用结果:

function add(a){

    var sum = 0;
    sum += a; 
    
    return function temp(b) { 
    
        if (arguments.length === 0) {
            return sum;
        } else {
            sum= sum+ b;
            return temp;
        }
    }
}

add(2)(3)(4)(5)(); //14
   

如果要使用匿名函数,也可以:

function add() {

        var _args = [];

        return function(){ 

            if(arguments.length === 0) { 
                return _args.reduce(function(a,b) {
                    return a + b;
                });
            }

            [].push.apply(_args, [].slice.call(arguments));

            return arguments.callee;
        }
    }

    var sum = add();
    sum(2,3)(4)(5)(); //14
      

2 . 利用JS中对象到原始值的转换规则。

当一个对象转换成原始值时,先查看对象是否有valueOf方法。
如果有并且返回值是一个原始值,那么直接返回这个值。
如果没有valueOf 或 返回的不是原始值,那么调用toString方法,返回字符串表示。

那我们就为函数对象添加一个valueOf方法 和 toString方法:

function add(a) {

    var sum = 0;
    
        sum += a;
        
    var temp = function(b) {
    
        if(arguments.length===0){
            return sum;
        } else {
            sum = sum+ b;
            return temp;
        }
        

    }
    
    temp.toString = temp.valueOf = function() {
        return sum; 
    }
    
    
    return temp;
}

add(2)(3)(4)(5); //14

写到这里,我们来简单总结下。

柯里化的定义
柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后,部分应用参数,并返回一个更具体的函数接受剩下的参数,中间可嵌套多层这样的接受部分参数函数,逐步缩小函数的适用范围,逐步求解,直至返回最后结果。
一个通用的柯里化函数
    var curring = function(fn){
        var _args = [];
        return function cb(){

            if(arguments.length === 0) {
                return fn.apply(this, _args);
            }

            Array.prototype.push.apply(_args, [].slice.call(arguments));

            return cb;
        }


    }

    var multi = function(){

        var total = 0;
        var argsArray = Array.prototype.slice.call(arguments);
            argsArray.forEach(function(item){
                total += item;
            })

        return total
    };

    var calc = curring(multi);

    calc(1,2)(3)(4,5,6);

    console.log(calc()); //空白调用时才真正计算

这样 calc = currying(multi),调用非常清晰.
如果要 累加多个值,可以把多个值作为做个参数 calc(1,2,3),也可以支持链式的调用,如 calc(1)(2)(3);

到这里, 不难看出,柯里化函数具有以下特点:

函数可以作为参数传递

函数能够作为函数的返回值

闭包

说了这么多,柯里化函数到底能够帮我做什么,或者说,我为什么要用柯里化函数呢? 我们接着往下看。

柯里化函数的作用
函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。
1.提高通用性
function square(i) {
    return i * i;
}

function double(i) {
    return i *= 2;
}

function map(handeler, list) {
    return list.map(handeler);
}

// 数组的每一项平方
map(square, [1, 2, 3, 4, 5]);
map(square, [6, 7, 8, 9, 10]);
map(square, [10, 20, 30, 40, 50]);
// ......

// 数组的每一项加倍
map(double, [1, 2, 3, 4, 5]);
map(double, [6, 7, 8, 9, 10]);
map(double, [10, 20, 30, 40, 50]);  

例子中,创建了一个map通用函数,用于适应不同的应用场景。显然,通用性不用怀疑。同时,例子中重复传入了相同的处理函数:square和dubble。

应用中这种可能会更多。当然,通用性的增强必然带来适用性的减弱。但是,我们依然可以在中间找到一种平衡。

看下面,我们利用柯里化改造一下:

function currying(fn) {
            var slice = Array.prototype.slice,
            __args = slice.call(arguments, 1);
            return function () {
                var __inargs = slice.call(arguments);
                return fn.apply(null, __args.concat(__inargs));
            };
        }

function square(i) {
    return i * i;
}

function double(i) {
    return i *= 2;
}

function map(handeler, list) {
    return list.map(handeler);
}

var mapSQ = currying(map, square);
mapSQ([1, 2, 3, 4, 5]); //[1, 4, 9, 16, 25]


var mapDB = currying(map, double);
mapDB([1, 2, 3, 4, 5]); //[2, 4, 6, 8, 10]

我们缩小了函数的适用范围,但同时提高函数的适性.

2 延迟执行。

柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。

3.固定易变因素。

柯里化特性决定了它这应用场景。提前把易变因素,传参固定下来,生成一个更明确的应用函数。最典型的代表应用,是bind函数用以固定this这个易变对象。
Function.prototype.bind = function(ctx) {
    var fn = this;
    return function() {
        fn.apply(ctx, arguments);
    };
};

Function.prototype.bind 方法也是柯里化应用与 call/apply 方法直接执行不同,bind 方法 将第一个参数设置为函数执行的上下文,其他参数依次传递给调用方法(函数的主体本身不执行,可以看成是延迟执行),并动态创建返回一个新的函数, 这符合柯里化特点。

var foo = {
        x: 666
    };
    
var bar = function () {
    console.log(this.x);
}.bind(foo); // 绑定

bar(); //666

    // 下面是一个 bind 函数的模拟,testBind 创建并返回新的函数,在新的函数中将真正要执行业务的函数绑定到实参传入的上下文,延迟执行了。
    Function.prototype.testBind = function (scope) {
        var self = this;   // this 指向的是调用 testBind 方法的一个函数, 
        return function () {
            return self.apply(scope);
        }
    };

    var testBindBar = bar.testBind(foo);  // 绑定 foo,延迟执行
    console.log(testBindBar); // Function (可见,bind之后返回的是一个延迟执行的新函数)
    testBindBar(); // 666
关于curry性能的备注
通常,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。

有关性能,这里有一些事情必须牢记于心:

存取arguments对象通常要比存取命名参数要慢一点.

一些老版本的浏览器在arguments.length的实现上是相当慢的.

创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上.

以上 ;)

参考资料

http://blog.jobbole.com/77956/
http://www.cnblogs.com/pigtai...

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

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

相关文章

  • 高级函数技巧-函数柯里

    摘要:如果你对函数式编程有一定了解,函数柯里化是不可或缺的,利用函数柯里化,可以在开发中非常优雅的处理复杂逻辑。同样先看简单版本的方法,以方法为例,代码来自高级程序设计加强版实现上面函数,可以换成任何其他函数,经过函数处理,都可以转成柯里化函数。 我们经常说在Javascript语言中,函数是一等公民,它们本质上是十分简单和过程化的。可以利用函数,进行一些简单的数据处理,return 结果,...

    shixinzhang 评论0 收藏0
  • JS基础之常用小技巧和知识总结(一)

    摘要:如果有一方是布尔值,则转换为,转换为,再进行判断。等同运算符类型不同返回类型相同如果同为数字字符串则比较值如果同为布尔值,相同则为不同为如果两个操作数同为引用类型,且引用的为同一个对象函数,数组,则相同。 本文主要记录平时开发遇到的知识点和小技巧 相等判断(==) 类型相同: 判断其值是否相同 类型不同: 1. 如果数字和字符串比较, 则字符串会被隐式转换为数字,在做判断。 2....

    dadong 评论0 收藏0
  • 高阶函数应用 —— 柯里与反柯里

    摘要:柯里化通用式上面的柯里化函数没涉及到高阶函数,也不具备通用性,无法转换形参个数任意或未知的函数,我们接下来封装一个通用的柯里化转换函数,可以将任意函数转换成柯里化。 showImg(https://segmentfault.com/img/remote/1460000018998373); 阅读原文 前言 在 JavaScript 中,柯里化和反柯里化是高阶函数的一种应用,在这之前...

    wyk1184 评论0 收藏0
  • 前端进击的巨人(五):学会函数柯里(curry)

    摘要:函数柯里化是把支持多个参数的函数变成接收单一参数的函数,并返回一个函数能接收处理剩余参数,而反柯里化就是把参数全部释放出来。但在一些复杂的业务逻辑封装中,函数柯里化能够为我们提供更好的应对方案,让我们的函数更具自由度和灵活性。 showImg(https://segmentfault.com/img/bVburN1?w=800&h=600); 柯里化(Curring, 以逻辑学家Has...

    chengtao1633 评论0 收藏0
  • JavaScript 函数式编程技巧 - 柯里

    摘要:作为函数式编程语言,带来了很多语言上的有趣特性,比如柯里化和反柯里化。在一些函数式编程语言中,会定义一个特殊的占位变量。个人理解不知道对不对延迟执行柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。作为函数式编程语言,JS带来了很多语言上的有趣特性,比如柯里化和反柯里化。 这里可以对照另外一篇介绍 JS 反柯里化 的文章一起看~ 1. 简介 柯里化(Currying)...

    edgardeng 评论0 收藏0

发表评论

0条评论

Kross

|高级讲师

TA的文章

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