函数的 柯里化和反柯里化

函数1

函数的柯里化

柯里化(currying)是把接收多个参数的函数变换成为接收一个部分参数的函数,并返回接收余下参数的新函数的技术。通常这个参数是一个。
可能我们对这个解释不太明白。 现在我们来思考一个简单的问题。 怎么定义一个只有一个参数的函数,实现加法运算。

function curry(a){ // 创建不销毁的作用域,保存参数。
    return function(b){ // 返回函数求和
        return a + b;
    }
}

var f = curry(1); // f只是一个函数
console.log(f(2));    // 3

其实我们把实现加法的函数转化成上面实例的函数就称为函数的柯里化。

函数柯里化

柯理化函数思想:一个js预先处理的思想;利用函数执行可以形成一个不销毁的作用域的原理,把需要预先处理的内容都储存在这个不销毁的作用域中,并且返回一个新函数,以后我们执行的都是新函数,在新函数中把之前预先存储的值进行相关的操作处理即可。
而且通常情况下,这些参数是具有相似性质的。 通常是用于求值。所以函数的柯里化又叫做部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
其实就是 f(a,b,c,d) => g(a)(b)(c)(d)() 的过程
或者 f(a,b,c,d) => g(a,b)(c,d)()

柯里化的演示

首先给三个数的加法运算。

function add_three(a, b, c){
    return a + b + c;
}

接着按照公式我们 将 f(a,b,c) ===> g(a)(b)(c)() 的形式。

我首先来改写成:

function add(a){ 
    return function(b){
        return function(c){
            return a + b + c;
        }
    }
}

但是这种写法可能不通用。当参数有很多的时候,可能嵌套的层次就更多了。 其实我们发现是利用闭包来储存了变量,那么我们换一种思路来存储参数。我们在改写:

var add = (function(){
    var args = [];  // 存放参数
    return function(){
        if(arguments.length === 0){  // 无参数就计算
            var sum = 0;
            for(var i = 0; i < args.length; i++){
                sum += args[i]
            }
            return sum;
        }else{  // 有参数就存起来
            [].push.apply(args, arguments);  // 只是存起来了
            return arguments.callee;  // 返回函数,方便链式调用
        }
    }
})()

在上面我们可以看到柯里化,还是重写了一个闭包函数,返回新函数来调用,而且我们的柯里化函数和被柯里化函数结合很紧密。 那么我们可不可以创建一个函数,这个函数就是用来柯里化其他函数。这样就可以做到解耦的做用了。

function currying(fn){
    var args = []; // 存放参数
    return function(){// 返回新函数,也就是柯里化之后的函数
        if(arguments.length === 0){  // 计算
            return fn.apply(fn,args);  // 调用之前的函数
        }else{ //  存储
            Array.prototype.push.apply(args, arguments);
            return arguments.callee; // 方便链式调用 g()()  这样的形式
        }
    } 
}

function add_three(a, b, c){
    return a + b + c;
}

var add = currying(add_three);
add(1);
console.log(add(2)(3)())

当然为了更加通用 我们还可以改成

function currying(fn, ...arg_init){
    let args = arg_init || [];  // 存储参数   并且保存 柯里化时传入的参数。
    return function() {
        if(arguments.length === 0){ // 判断计算时机
            Let _args = args;
            args = []; // 将参数组置空, 避免重复计算。
            return fn.apply(this, _args); // 调用柯里化的函数
        }else{ // 缓存参数
            [].push.apply(args, arguments);
            return arguments.callee;
        }
    }
}

function add_(...args){
    var sum = 0;
    for(var i = 0; i < args.length; i++){
        sum += args[i];
    }
    return sum;
}

var add = currying(add_three,1);
add(2);
add(3,4);
add(5)(6);
console.log(add())

柯里化的作用

其实,在上面过程中我们也看到了,函数柯里化的特点了。

  1. 参数是具有相似性的。
  2. 缓存数据,需要是在计算。
  3. 可以将某些参数提前固定。(var add = currying(add_three,1);) 就像这样的。。提前固定参数

那么接下来我们看看柯里化函数有哪些应用场景

  1. 缩小函数适用范围,提高函数适用性(减少重复传递不变的部分参数)
function simpleURL(protocol, domain, path) {
    return protocol + "://" + domain + "/" + path;
}
var myurl = simpleURL('http', 'mysite', 'home.html');
var myurl2 = simpleURL('http', 'mysite', 'aboutme.html');

通过分析
就会发现有时候,我们在用一些函数,会重复传递一些相同的参数,但是我们又不能因为自己就去改成函数,那么我们就可以利用柯里化来限制参数,返回新函数来调用。

myURL = currying(simpleURL,"http",'mysite');
myURL('home.html');
myURL('aboutme.html')
  1. 延迟执行
    就是之前求和的案例

  2. 固定异变因素
    提前把易变因素,传参固定下来,生成一个更明确的应用函数。最典型的代表应用,是bind函数用以固定this这个易变对象。
Function.prototype.bind = function(context) {
    var _this = this;
    var _args = Array.prototype.slice.call(arguments, 1);        
    return function() {
        return _this.apply(context, _args.concat( Array.prototype.slice.call(arguments)));
    }
}

反柯里化

Array.prototype上的方法原本只能用来操作array对象。但用call和apply可以把任意对象当作this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性
有没有办法把泛化this的过程提取出来呢?反柯里化(uncurrying)就是用来解决这个问题的。反柯里化主要用于扩大适用范围,创建一个应用范围更广的函数。使本来只有特定对象才适用的方法,扩展到更多的对象。
这里也就是要把obj.fun(arg1, arg2) 转换成为 fun1(obj, arg1, arg2) 的形式。其实就是,调用 uncurrying 并传入一个现有函数 fn, 反柯里化函数会返回一个新函数,该新函数接受的第一个实参将绑定为 fn 中 this的上下文,其他参数将传递给 fn 作为参数。
代码实现如下;

  1. 第一种:
function uncurrying(fn){
    return function(){  // 返回一个 反柯里化函数的新函数
        var args = [].slice.call(arguments,1); // 获取参数数组, 因为第一参数是我们传入的对象本身。
        return fn.apply(arguments[0],args); // 其实本质还是对象调用原本的函数
    }
}

举个例子来验证一下(数组的push)

push = uncurrying(Array.prototype.push);
var arr = [1];
push(arr,2);
console.log(arr); // [1,2]

其实,我们想要的是第一个参数和后面的参数分割开。 所以上述的反柯里化函数还可以该改写成为:

function uncurrying(fn){
    return function(){  // 返回一个 反柯里化函数的新函数
        var context=[].shift.call(arguments);  // arguments去除第一个参数, 并赋值给新变量
        return fn.apply(context, arguments); // 其实本质还是对象调用原本的函数
    }
}
  1. 第二种:
    那么我知道 call 方法和apply方法功能一样,只不过参数不一样。所以可以写成:
var uncurrying= function (fn) {
    return function () {        
        return fn.call(...arguments); // es6的语法规则, 自动做参数展开,正好划分了参数
    }
};
  1. 第三种
    同样这是你常见的一种形式:(就是call也可以调用 apply 函数, 做参数的拆分)
var uncurrying= function (fn) {
    return function () {        
        return Function.prototype.call.apply(fn,arguments);
    }
};

由此可见 Function.prototype.call.apply(fn,arguments) 相当于 fn.call(arguments[0],argumnet[1]....)

  1. 第四种
    接下来,我们知道bind是会返回一个函数,他和call类似,只不过不立即执行而是返回函数。那么我们先去内层函数包裹(也就是第一个return),改写(对第2种):
    我们知道bind也是具有和call以及apply的性质,而且他还返回一个函数。之前我们也介绍过了,bind其实是柯里化的函数,具有指定参数的作用,返回的新函数具有接收剩余参数的功能。
    针对第二种的改写(这个是方便你去理解这种形式的。。你可以参考 bind的 实现源码来看,上文有介绍)
var uncurrying= function (fn) {
    return fn.call.bind(arguments[0]); // es6的语法规则
};

第三种的改写,去掉内层函数

// 用函数表达式
var uncurrying = function(fn) {
    return Function.prototype.call.bind(fn);    
};

其实,也就是你经常见到的一种形式, push = Function.prototype.call.bind(Array.prototype.push); 的一层函数封装。

其实还是一种你常见的简洁形式。

  1. 第五种
var uncurrying = Function.prototype.bind.bind(Function.prototype.call);

.
.
.
不解释了。。。。

总结

其实学习是相互印证的过程,之前也写过bind的用法,可能还是停留在一个初级阶段,记忆和简单会用,直到写完这篇才算是对bind有了一个深刻的认识吧。尤其是最后的反柯里化函数的简写形式,真的是隐藏了很多东西。
阿姨不知道你能不能看明白...

算了还是解释下第五种吧:
var uncurrying = Function.prototype.bind.bind(Function.prototype.call);
其实利用bind的源码实现原理,来去掉最后一个bind
就是变成下面的代码了,

var uncurrying = function(){
    return Function.prototype.bind.apply(Function.prototype.call,arguments)
}

我们之前介绍过了apply的特性了,其实就相当于给第一个对象身上绑定了一个方法,调用结束之后删除该方法。 同理,上述代码就相当于给call绑定了bind的方法。call去调用bind的方法。 且将agrguments参数展开传入(这里也是有细节的), 所以就变成了下面的样子。

var uncurrying = function(){
    return Function.prototype.call.bind(...arguments);
}

因为我们传入的参数是一个,所以就变成这样了, 是不是就是和第三种一样了?

var uncurrying = function(fn){
    return Function.prototype.call.bind(fn);
}

接着在去掉bind

var uncurrying = function(fn){
    return function(){
        Function.prototype.call.apply(fn,arguments);
    }
}

也就是(是不是很熟悉了) 这里之所以可以这么写,利用了一点技巧,偷换了一点概念。

var uncurrying = function(fn){
    return function(){
        fn.call(...arguments);
    }
}

猜你喜欢

转载自www.cnblogs.com/cyrus-br/p/10528808.html
今日推荐