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

一、函数柯里化

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

例:记录程序员一个月的加班总时间,那么好,我们首先要做的是记录程序员每天加班的时间,然后把一个月中每天的加班的时间相加,就得到了一个月的加班总时间。

var monthTime = 0;
function overtime(time) {
    return monthTime += time;
}
overtime(3.5);    // 第一天
overtime(4.5);    // 第二天
overtime(2.1);    // 第三天
//...
console.log(monthTime);    // 10.1

下面的overtime函数还不是一个柯里化函数的完整实现,但可以帮助我们了解其核心思想:

var overtime = (function() {
    var args = [];
    return function() {
        if(arguments.length === 0) {
            var time = 0;
            for (var i = 0, len = args.length; i < len; i++) {
                time += args[i];
            }
            return time;
        }
        else {
            [].push.apply(args, arguments);
        }
    }
})();
overtime(3.5);    // 第一天
overtime(4.5);    // 第二天
overtime(2.1);    // 第三天
//...
console.log( overtime() );    // 10.1

例:使用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参数。即只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
  1. 使用currying的好处

(1)参数复用

/*正常正则验证字符串 reg.test(txt)*/
// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}
console.log(check(/\d+/g, 'test'));   //false
console.log(check(/[a-z]+/g, 'test'));   //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}
var hasNumber = curryingCheck(/\d+/g);
var hasLetter = curryingCheck(/[a-z]+/g);
console.log(hasNumber('test1'));      // true
console.log(hasNumber('testtest'));   // false
console.log(hasLetter('21212'));      // false
  • 以上是一个正则的校验,正常来说直接调用check函数就可以,但是如果有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

(2)提前确认

//第一种:最常见封装dom的方法
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() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } 
    else {
        return function(element, event, handler) {
            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);
    }
}

(3)延迟运行

js中经常使用的bind,实现的机制就是currying:

Function.prototype.bind=function(context){  //context为我们想修正的this对象
    var self=this;  //保存原函数
    var args = Array.prototype.slice.call(arguments, 1);
    return function(){  //返回一个新函数,实际上执行时会先执行这个新函数
        return self.apply(context,args);  //执行原函数,指定context为原函数体内的this
    }
};

通用的封装方式:

// 初步封装
var currying = function(fn) {  //fn为要进行柯里化的函数
    var args = Array.prototype.slice.call(arguments, 1);  //args获取第一个方法内的全部参数
    return function() {
        var newArgs = args.concat(Array.prototype.slice.call(arguments));  //将后面方法里的全部参数和args进行合并
        return fn.apply(this, newArgs);  //把合并后的参数通过apply作为fn的参数并执行
    }
}
//使用方式
function add(num1,num2){
    return num1*num2;
}
var curriedAdd=currying(add,5);
console.log(curriedAdd(8));  //40
  • 通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

多参数的封装:

// 支持多参数传递
function progressCurrying(fn, args) {
    var self = this;
    var len = fn.length;
    var args = args || [];
    return function () {
        var newArgs = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, newArgs);
        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (newArgs.length < len) {
            return progressCurrying.call(self, fn, newArgs);
        }
        return fn.apply(this, newArgs);  // 参数收集完毕,则执行fn
    }
}
  • 在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。
  1. curry的性能
  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
  1. 练习题
function add() {
    var args = Array.prototype.slice.call(arguments);  // 第一次执行时,定义一个数组专门用来存储所有的参数
    var adder = function() {
        args.push(...arguments);  // 在内部声明一个函数,利用闭包的特性保存args并收集所有的参数值
        return adder;
    };
    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    adder.toString = function () {
        return args.reduce(function (a, b) {
            return a + b;
        });
    };
    return adder;
}
console.log(add(1)(2)(3));   //6
console.log(add(1,2,3)(4));   // 10
console.log(add(1)(2)(3)(4)(5));   // 15
console.log(add(2,6)(1));   // 9

二、函数反柯里化

当我们调用对象的某个方法时,不用去关心该对象原本是否被设计为拥有这个方法,只要这个方法适用于它,我们就可以对这个对象使用它。

例:使用uncurrying将泛化this的过程提取出来

//分析一下调用Array.prototype.push.uncurring()这句代码时,发生了什么事情:
Function.prototype.uncurring = function() {
    var self = this;  //self此时是Array.prototype.push
    return function() {
        var obj = Array.prototype.shift.call(arguments);
        //obj 是{
        //  "length": 1,
        //  "0": 1
        //}
        //arguments的第一个对象被截去(也就是调用push方法的对象),剩下[2]
        return self.apply(obj, arguments);   //相当于Array.prototype.push.apply(obj, 2);    
    };
};
//测试一下
var push = Array.prototype.push.uncurring();
var obj = {
    "length": 1,
    "0" : 1
};
push(obj, 2);
console.log( obj ); //{0: 1,1: 2, length: 2 }

另一种实现方式:

Function.prototype.uncurring=function(){
    var self=this;
    return function(){
        return Function.prototype.call.apply(self,arguments);
    }
};

例:把Array.prototype.push方法转换成一个通用的push函数

 var push = Array.prototype.push.uncurring();
//测试一下
//arguments本来是没有push方法的,通常,我们都需要用Array.prototype.push.call来实现push方法,但现在,直接调用push函数,既简洁又意图明了。
(function() {
    push(arguments, 4);
    console.log(arguments); //[1, 2, 3, 4]
})(1, 2, 3);

你可能感兴趣的:(js)