红宝书笔记——函数柯里化

313 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

今天在红宝书上翻到一个名词:函数柯里化

解释

查到一个比较官方的解释:

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

函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。

柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。
// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

费劲半天,这么做是为什么呢?

柯里化的好处

参数复用

正常正则验证字符串

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后

柯里化验证

function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

const hasNumber = curryingCheck(/\d+/g);
const hasLetter = curryingCheck(/[a-z]+/g);

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

正常来说直接调用check函数就可以了,但是如果很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便了。

(其他的好处,我目前还没研究透彻o(╥﹏╥)o)

柯里化通用的封装方法

image.png

// 初步封装
var currying = function(fn) {
    // args 获取第一个方法内的全部参数
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        // 将后面方法里的全部参数和args进行合并
        var newArgs = args.concat(Array.prototype.slice.call(arguments))
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs)
    }
}

首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

如果有多个参数的时候,需要加上递归再封装一层:

// 支持多参数传递
function progressCurrying(fn, args) {

    var _this = this
    var len = fn.length;
    var args = args || [];

    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

curry的性能

  • 存取arguments对象通常要比存取命名参数要慢一些;

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

  • 使用fn.apply( … ) fn.call( … )通常比直接调用fn( … ) 稍微慢点;

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

面试题

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

参考答案:

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    let _args = Array.prototype.slice.call(arguments);
    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    let sumNum = function() {
        _args.push(...arguments);
        return sumNum;
    };
    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    sumNum.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return sumNum;
}