JS 函数柯里化

161 阅读5分钟

柯里化:看到这个词就感觉很高端,实际上当你了解后才发现其实就是高阶函数的一个特殊用法。

果然不管作用怎么样都要有个高端的名字才有用。

首先看看柯里化到底是什么?

维基百科上说道:柯里化,英语: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 参数。现在思路应该比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

但费这么大劲封装一层,有什么用处呢?

来列一列 Currying 有哪些好处

1、参数复用

//正常正则验证字符串  reg.test(txt)

//普通函数封装后
function check(reg , txt){
    return reg.text(txt);
}


// Currying 后
function curryingCheck(reg){
    return function(txt){
        return reg.text(txt);
    }
}
var hasNum = curryingCheck(/\d+/g);
var hasLetter = curryingCheck(/[a-z]+/g);

hasNum('123')
hasLetter('fadf')

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

2、提前确认

var on = function(element, event , handler){
    if(document.addEventListener){
        if(element && event && handler){
            element.addEventListener(event , handler , false);
        }
    }else{
        if(element && event && handler){
            element.attachEvent('on'+ event , handler );
        }
    }
}


var on = (function(element , event , handler){
    if(document.addEventListener){
        if(element && event && handler){
            element.addEventListener(event , handler , false);
        }
    }else{
        if(element && event && handler){
            element.attachEvent('on'+ event , handler );
        }
    }
})();

// 换种写法可能比较好理解一点,上面就是把 isSupport 这个参数给先确定下来了
var on = function(isSupport , element , event , handler){
    isSupport = isSupport || document.addEventListener; 
    if(isSupport){
        return element.addEventListener(event , handler , false);
    }else {
        return element.attachEvent('on'+ event , handler );
    }
}

我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法比较常见,但是我们看看第二种写法,它相对第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

3、延迟执行

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

JS中经常使用的 bind 实现的机制就是 Currying。

这时候会发现一个问题,难道每次使用Currying都要对底层函数去做修改??

通用的封装方法

// 初步封装
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 的函数。

细心的朋友们应该发现有上面代码的问题:它只能多扩展出一个参数,也就是对 currying(a)(b)(c) 这样不支持,一般情况都会想到使用递归再进行封装一层。

// 支持多参数传递
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);
    }
}

这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的 fn.length 就会继续执行递归 。

有了上面的能用方法,但作为一个程序员,咱也得关注一下它的性能问题

currying 的性能说明

对于 currying 的一些性能问题主要有以下四个点:

  • 存取 arguments 对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在 arguments.length 的实现上是相当慢的
  • 使用 fn.apply(...)fn.call(...) 通常比直接调用 fn(...) 稍慢
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

但其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,所以 curry 是可以直接放心使用的。

常见面试题

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

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9