深入详解函数的柯里化

柯里化概念非常重要

不错鼓励并赞赏 标签: Javascript      评论 / 2019-09-15


柯里化是函数的一个比较高级的应用,想要理解它并不简单。

补充知识点之函数的隐式转换

JavaScript作为一种弱类型语言,它的隐式转换是非常灵活有趣的。当我们没有深入了解隐式转换的时候可能会对一些运算的结果会感动困惑,比如4 + true = 5。当然,如果对隐式转换了解足够深刻,肯定是能够很大程度上提高对js的使用能力。

举例说明:

    function fn() {
        return 20;
    }
     
    console.log(fn + 10); // 输出结果是多少?

稍微修改一下,再想想输出结果会是什么

    function fn() {
        return 20;
    }
     
    fn.toString = function() {
        return 10;
    }
     
    console.log(fn + 10);  // 输出结果是多少?

还可以继续修改一下。

    function fn() {
        return 20;
    }
     
    fn.toString = function() {
        return 10;
    }
     
    fn.valueOf = function() {
        return 5;
    }
     
    console.log(fn + 10); // 输出结果是多少?

以上输出结果为:

    // 输出结果分别为
    function fn() {
        return 20;
    }10
     
    20
     
    15

当我们没有重新定义toString与valueOf时,函数的隐式转换会调用默认的toString方法,它会将函数的定义内容作为字符串返回。而当我们主动定义了toString/vauleOf方法时,那么隐式转换的返回结果则由我们自己控制了。其中valueOf的优先级会toString高一点。

补充知识点之利用call/apply封数组的map方法

map(): 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。

通俗来说,就是遍历数组的每一项元素,并且在map的第一个参数(回调函数)中进行运算处理后返回计算结果。返回一个由所有计算结果组成的新数组。

    // 回调函数中有三个参数
    // 第一个参数表示newArr的每一项,第二个参数表示该项在数组中的索引值
    // 第三个表示数组本身
    // 除此之外,回调函数中的this,当map不存在第二参数时,this指向丢失,当存在第二个参数时,指向改参数所设定的对象
    var newArr = [1, 2, 3, 4].map(function(item, i, arr) {
        console.log(item, i, arr, this);  // 可运行试试看
        return item + 1;  // 每一项加1
    }, { a: 1 })
     
    console.log(newArr); // [2, 3, 4, 5]

在上面例子的注释中详细阐述了map方法的细节。现在要面临一个难题,就是如何封装map。

可以先想想for循环。我们可以使用for循环来实现一个map,但是在封装的时候,我们会考虑一些问题。我们在使用for循环的时候,一个循环过程确实很好封装,但是我们在for循环里面要对每一项做的事情却很难用一个固定的东西去把它封装起来。因为每一个场景,for循环里对数据的处理肯定都是不一样的。

于是大家就想了一个很好的办法,将这些不一样的操作单独用一个函数来处理,让这个函数成为map方法的第一个参数,具体这个回调函数中会是什么样的操作,则由我们自己在使用时决定。因此,根据这个思路的封装实现如下。

    Array.prototype._map = function(fn, context) {
        var temp = [];
        if(typeof fn == 'function') {
            var k = 0;
            var len = this.length;
            // 封装for循环过程
            for(; k < len; k++) {
                // 将每一项的运算操作丢进fn里,利用call方法指定fn的this指向与具体参数
                temp.push(fn.call(context, this[k], k, this))
            }
        } else {
            console.error('TypeError: '+ fn +' is not a function.');
        }
     
        // 返回每一项运算结果组成的新数组
        return temp;
    }
     
    var newArr = [1, 2, 3, 4]._map(function(item) {
        return item + 1;
    })
    // [2, 3, 4, 5]

在上面的封装中,首先定义了一个空的temp数组,该数组用来存储最终的返回结果。在for循环中,每循环一次,就执行一次参数fn函数,fn的参数则使用call方法传入

在理解了map的封装过程之后,我们就能够明白为什么我们在使用map时,总是期望能够在第一个回调函数中有一个返回值了。在eslint的规则中,如果我们在使用map时没有设置一个返回值,就会被判定为错误。

ok,明白了函数的隐式转换规则与call/apply在这种场景的使用方式,我们就可以尝试通过简单的例子来了解一下柯里化了

demo 1:


实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15


OK,答案在这里,当然这是其中一种解决问题的方案:

    function add() {
        // 第一次执行时,定义一个数组专门用来存储所有的参数
        var _args = [].slice.call(arguments);
     
        // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
        var adder = function () {
            var _adder = function() {
                [].push.apply(_args, [].slice.call(arguments));
                return _adder;
            };
     
            // 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
            _adder.toString = function () {
                return _args.reduce(function (a, b) {
                    return a + b;
                });
            }
     
            return _adder;
        }
        return adder.apply(null, [].slice.call(arguments));
    }
     
    // 输出结果,可自由组合的参数
    console.log(add(1, 2, 3, 4, 5));  // 15
    console.log(add(1, 2, 3, 4)(5));  // 15
    console.log(add(1)(2)(3)(4)(5));  // 15

再次普及一下知识点

柯里化(英语:Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。

  • 接收单一参数,因为要携带不少信息,因此常常以回调函数的理由来解决。

  • 将部分参数通过回调函数等方式传入函数中

  • 返回一个新函数,用于处理所有的想要传入的参数

在上面的例子中,我们可以将add(1, 2, 3, 4)转换为add(1)(2)(3)(4)。这就是部分求值。每次传入的参数都只是我们想要传入的所有参数中的一部分。当然实际应用中,并不会常常这么复杂的去处理参数,很多时候也仅仅只是分成两部分而已。

柯里化通用式

通用的柯里化写法其实比我们上边封装的add方法要简单许多。

    var currying = function(fn) {
        var args = [].slice.call(arguments, 1);
    
        return function() {
            // 主要还是收集所有需要的参数到一个数组中,便于统一计算
            var _args = args.concat([].slice.call(arguments));
            return fn.apply(null, _args);
        }
    }
    
    var sum = currying(function() {
        var args = [].slice.call(arguments);
        return args.reduce(function(a, b) {
            return a + b;
        })
    }, 10)
    
    console.log(sum(20, 10));  // 40
    console.log(sum(10, 5));   // 25

柯里化与bind

    Object.prototype.bind = function(context) {
        var _this = this;
        var args = [].prototype.slice.call(arguments, 1);
     
        return function() {
            return _this.apply(context, args)
        }
    }

这个例子利用call与apply的灵活运用,实现了bind的功能。

在前面的几个例子中,我们可以总结一下柯里化的特点:

  • 接收单一参数,将更多的参数通过回调函数来搞定

  • 返回一个新函数,用于处理所有的想要传入的参数

  • 需要利用call/apply与arguments对象收集参数

  • 返回的这个函数正是用来处理收集起来的参数

Hi 看这里!

大家好,我是PRO

我会陆续分享生活中的点点滴滴,当然不局限于技术。希望笔墨之中产生共鸣,每篇文章下面可以留言互动讨论。Tks bd!

博客分类

您可能感兴趣

作者推荐

呃,突然想说点啥

前端·博客

您的鼓励是我前进的动力---

使用微信扫描二维码完成支付