Zerlinda's Blog

浅谈js函数柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel Gottlob Frege 发明的。

在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。

以上引用百度百科的词解释。通俗讲,一个currying的函数接收一些参数,接收了这些参数之后,该函数并不是立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来,待到函数真正需要求值的时候,之前传入的所有参数都能用于求值。

通用实现:

function currying(fn) {
    var slice = Array.prototype.slice,
    _args = slice.call(arguments, 1);
    return function () {
        var _inargs = slice.call(arguments);
        return fn.apply(null, _args.concat(_inargs));
    };
}

仅仅看通用函数很难看懂究竟为何要柯里化以及通用函数的意义是什么,举几个例子,柯里化的实用性体现在很多方面:

1 提高适用性。

对不同的应用场景往往要传递很多参数来解决特定问题。有时候应用中,同一种规则可能会反复使用,这就可能会造成代码的重复性。通用函数解决了兼容性问题,但同时也会带来使用的不便利性。

看下面一个例子:

function square(i) {
    return i * i;
}
function dubble(i) {
    return i *= 2;
}
function map(handeler, list) {
    return list.map(handeler);
}
// 数组的每一项平方
map(square, [1, 2, 3, 4, 5]);
map(square, [6, 7, 8, 9, 10]);
map(square, [10, 20, 30, 40, 50]);
// 数组的每一项加倍
map(dubble, [1, 2, 3, 4, 5]);
map(dubble, [6, 7, 8, 9, 10]);
map(dubble, [10, 20, 30, 40, 50]);

上例中创建了两个应用函数squaredouble,同时创建了map通用函数,用于适应这两个不同的应用场景(数组加倍、数组求平方)。

实际应用中除了squaredouble可能会包含更多的应用函数,因此我们每次调用map函数都要像上面一样不停的重复输入squaredouble……通用性的增强必然带来适用性的减弱。但是,我们依然可以在中间找到一种平衡。

利用柯里化改造一下:

function currying(fn) {
    var slice = Array.prototype.slice,
    _args = slice.call(arguments, 1);
    return function () {
        var _inargs = slice.call(arguments);
        return fn.apply(null, _args.concat(_inargs));
    };
}
function square(i) {
    return i * i;
}
function map(handeler, list) {
    return list.map(handeler);
}
var mapSQ = currying(map, square);
mapSQ([1, 2, 3, 4, 5]);
mapSQ([6, 7, 8, 9, 10]);
mapSQ([10, 20, 30, 40, 50]);

柯里化后返回的依然是一个函数,因此我们依旧可以继续调用该函数去传入更多的参数。在上面的例子中,mapSQ中传入了参数:应用函数mapsquare并返回一个新的function,第一个参数map是最终需要执行的函数,在返回函数的同时使用闭包保存了需要传入map的第一个参数square。而下一步执行mapSQ时同时传入了map的第二个参数:数组([1,2,3,4,5]),并且将square和该数组进行合并成为最后的参数,使用apply传入map函数。

柯里化的第一个参数固定缩小了函数的适用范围,但同时提高函数的适性。由此,可知柯里化不仅仅是提高了代码的合理性,更重的它突出一种思想—降低适用范围,提高适用性。接下来的例子都是大同小异,就不做太多解释。

一个应用范围更广泛更熟悉的例子:

function Ajax() {
    this.xhr = new XMLHttpRequest();
}
Ajax.prototype.open = function(type, url, data, callback) {
    this.onload = function() {
        callback(this.xhr.responseText, this.xhr.status, this.xhr);
    }
    this.xhr.open(type, url, data.async);
    this.xhr.send(data.paras);
}
'get post'.split(' ').forEach(function(mt) {
    Ajax.prototype[mt] = currying(Ajax.prototype.open, mt);
});
var xhr = new Ajax();
xhr.get('/articles/list.php', {},
function(datas) {
    // done(datas)    
});

var xhr1 = new Ajax();
xhr1.post('/articles/add.php', {},
function(datas) {
    // done(datas)    
});

2 延迟执行。

柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。

通用的写法:

var curry = function(fn) {
    var _args = []
    return function cb() {
        if (arguments.length == 0) {
            return fn.apply(this, _args)
        }
        Array.prototype.push.apply(_args, arguments);
        return cb;
    }
}

下面一个例子:

已知 fn 为一个固定了参数个数的函数,实现函数 curryIt,调用之后满足如下条件:

function fn(a, b, c) {
     return a + b + c
}; 
curryIt(fn)(1)(2)(3);      //6

function curryIt(fn) {
     var length = fn.length,
        args = [];
       return function cb(arg){
             args.push(arg);
             if(args.length < length){
                   return cb;
         }else {
             return fn.apply(null,args);
         }
       }
}

再看另外一个例子,函数add中的参数不固定:

var add = function() {
  var _args = [].slice.apply(arguments), 
      sum = 0;
  _args.forEach(function(value){
    sum += value;
  })
  return sum;
}
var curry = function(fn) {
  var _args = []
  return function cb() {
    if (arguments.length == 0) {
      return fn.apply(null, _args)
    }else{
      [].push.apply(_args, arguments);
      return cb;
    }
  }
}
console.log(curry(add)(1)(2)(3)(6)())    //12

上面两个例子最大的不同就是应用函数的参数个数,一个(fn)是固定个数,另一个(add)不可估计。不同的需求柯里化的过程也是不一样的。上面的例子,对于固定个数的应用函数,使用function.length返回形参的个数,因此只需要在每次合并参数数组的时候判断数组的length来确定最终返回的是函数对象还是函数执行的结果。而对于不可估计参数个数的应用函数,在合并参数数组的时候要判断传入的参数是否为空,这就是为什么add的柯里化函数执行的时候要多一个()的原因;

3 固定易变因素。

柯里化特性决定了它这应用场景。提前把易变因素,传参固定下来,生成一个更明确的应用函数。最典型的代表应用,是bind函数用以固定this这个易变对象。

var o = {
  name: "tom",
};
function sayHi(){
  console.log("hi~",this.name);
}
Function.prototype.bind = function(context) {
  var _this = this,     //Function对象的属性this指向Function
      _args =[].slice.call(arguments, 1);
   return function() {
    return _this.apply(context, _args.concat([]slice.call(arguments)))
  }
}
var say = sayHi.bind(o);
say();    //hi~tom

Function本身是对象,对象方法的this指向的是当前对象(this.apply == sayHi.apply)就不用我多说了吧。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注