JS对象类型函数进阶篇函数柯里化

函数柯里化currying的概念最早由俄国数学家Moses Schönfinkel发明,而后由著名的数理逻辑学家Haskell Curry将其丰富和发展,currying由此得名。

定义

currying又称部分求值。柯里化函数首先会接受一些参数,参数接收之后不会立即求值,而是继续返回一个新的函数,之前传入的参数会在新函数形成的闭包中保存起来,等到函数真正需要求值的时候,之前传入的所有参数会被一次性用于求值。

概念不好理解,举例说明

【计算每月的开销】有一项任务,要求计算出每月的开销。最简单的方法肯定是记录下每天的开销,然后月末的时候加到一起就行了,于是可以这样写:

var totalMoney = 0;

var cost = function(money) {
  totalMoney += money
}

cost(100) // 第1天消费
cost(200) // 第2天消费
// ...
cost(100) // 第30天消费
console.log(totalMoney) // 当月消费金额

上面的函数计算了每天的开销,但是实际我只需要知道月末的消费就好了,其他时间的消费总和我并不关心,于是可以这样改写:

var cost = (function(){
var args = [];
return function() {
 if(arguments.length === 0) { // 如果没有参数,求和并返回结果
   var totalMoney = 0;
   for(var i = 0, len = args.length; i < len; i++) {
      totalMoney += args[i]
   }
   return totalMoney
 } else { // 如果有参数,把参数添加到数组
   [].push.apply(args, arguments)
 }
}
})()

cost(100) // 不求值
cost(200) // 不求值
cost(100) // 不求值
cost() // 400

举这个例子只是便于理解概念,并不能体现柯里化函数的强大之处,它真正的用处是函数式编程。

通用函数

把上面的例子改写成一个通用的柯里化函数,把一个将要被柯里化的函数作为参数。

var currying = function (fn) {
    var args = [];
    return function () {
      if (arguments.length === 0) { // 如果没有传参就进行求值
        return fn.apply(this, args);
      } else {
        // 把fn的参数展开,添加到数组里面
        [].push.apply(args, arguments);
      }
    }
  };

  var totalCost = (function () {
    var totalMoney = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        totalMoney += arguments[i];
      }
      return totalMoney;
    }
  })();

  var cost = currying(totalCost); // 转化成 currying 函数
  cost(100); // 未真正求值 
  cost(200); // 未真正求值 
  cost(100); // 未真正求值 
  console.log(cost()); // 400 

可传参的函数

柯里化函数不仅可以接受一个将要被柯里化的函数作为参数,也可以接受一些参数。

  var currying = function (fn) {
    var args = [].slice.call(arguments, 1); // 获取除fn之外的其他参数
    return function () {
      if (arguments.length === 0) {
        return fn.apply(this, args);
      } else {
        [].push.apply(args, arguments);
      }
    }
  };

  var totalCost = (function () {
    var totalMoney = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        totalMoney += arguments[i];
      }
      return totalMoney;
    }
  })();

  var cost = currying(totalCost, 100, 200);
  cost(100);
  cost(200);
  cost(100);
  console.log(cost()); // 700  

求值柯里化

柯里化函数传参的同时,可以进行求值。

  var currying = function (fn) {
    var args = [].slice.call(arguments, 1);// 获取除fn外的其他参数
    return function () {
     // 把fn里面的参数转换为数组
     var innerArgs = [].slice.call(arguments)
     // 合并参数
     var finalArgs = args.concat(innerArgs);
     // 把参数传给fn函数
     return fn.apply(null, finalArgs);
    }
  };

  var totalCost = (function () {
    var totalMoney = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        totalMoney += arguments[i];
      }
      return totalMoney;
    }
  })();

  var cost = currying(totalCost, 100, 200);
  console.log(cost(100)); // 100 + 200 + 100 = 400
  console.log(cost(100,200)); // 400 + 100 + 200 + 100 + 200 = 1000 

反柯里化

介绍call()和apply()方法时说过,它们都可以改变函数中this的指向,这也是为什么数组的方法可以用到对象上。反柯里化就是为了把泛化的this提取出来,扩大方法的适用范围,使本来只能用于特定对象的方法扩展到更多的对象。

下面是两种实现uncurrying的方法:

Function.prototype.uncurrying = function(){
  var _this = this;
  return function(){
    return Function.prototype.call.apply(_this, arguments)
  }
}
Function.prototype.uncurrying = function(){
  var _this = this;
  return function(){
    var obj = Array.prototype.shift.call(arguments)
      return _this.apply(obj, arguments)
    }
}

这两种方式的原理是一样的,都是把this.method(arg1, arg2)转换成method(this, arg1, arg2)

【示例】让对象拥有push()方法

var push = [].push.uncurrying(), obj = {}, arr = [];
push(obj, 'hello', 'world')
push(arr, 'hello', 'world')
console.log(obj, arr) // {0: 'hello', 1: 'world', length: 2}  ['hello', 'world']

【示例】让数组拥有toUpperCase方法

var toUpperCase = String.prototype.toUpperCase.uncurrying(), arr = ['hello','world'];
console.log(arr.map(toUpperCase)) // ['HELLO', 'World']
优秀文章首发于聚享小站,欢迎关注!
原文地址:https://www.cnblogs.com/yesyes/p/15351910.html