Skip to content

JS 基础:柯里化与反柯里化

柯里化

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

柯里化,作为高阶函数的一种应用,是一个逐步接收参数的过程。

js
var add = (x, y) => x + y;

var curriedAdd = (x) => (y) => x + y;
var addOne = curriedAdd(1);

var addTen = curriedAdd(10);

addOne(5);
// 6

addTen(5);
// 15

上述示例,将原本接收两个参数的 add 改造成了接收一个参数的 addOneaddTen 函数,每个函数都具有独立的语义。

这种方式有一个优点,可以把易变的参数固定下来。这个最典型的应用场景是使用 bind 函数(偏函数的一种实现)绑定 this 对象:

js
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)));
  }
}

柯里化的另一个应用场景是在如果有多个不同的执行场景,可以提前确定当前执行环境。这个最典型的例子是兼容现代浏览器以及 IE 浏览器的事件监听:

js
var addEvent = (root, ele, type, fn, capture = false) => {
  if (root.attachEvent) {
    ele.attachEvent('on' + type, fn);
  } else if (root.addEventListener) {
    ele.addEventListener(type, fn, capture);
  }
};

这个函数执行倒没有问题,只是每次调用都会执行一次 if...else,挺繁琐的,完全可以通过柯里化只做一次判定:

js
var addEvent = ((root) => {
  if (root.attachEvent) {
    return (ele, type, fn) => ele.attachEvent('on' + type, fn);
  } else if (root.addEventListener) {
    return (ele, type, fn, capture = false) => ele.addEventListener(type, fn, capture);
  }
})(window);

这样 addEvent 函数实际上已经是本浏览器支持的事件添加方法。

curry 的实现

curry 的实现有两种方法,第一种是自己手工实现:

js
var add = (x, y, z) => x + y + z;

var curriedAdd = (x) => (y) => (z) => x + y + z;

还有一种方法就是使用 curry 函数进行转换, 如 lodashramda 都提供有函数可以自动完成 curry, 这里写一个简单的实现:

js
var curry = (fn, length = fn.length) => {
  var args = [];
  var _curryN = (...arguments) => {
    args = args.concat(Array.prototype.slice.call(arguments));
    if (args.length == length) {
      return fn.apply(null, args);
    }
    return _curryN;
  };
  return _curryN;
};

就是,生成一个 curry 函数,每次调用 curry 函数的时候计算函数的参数是否满足定义时候的参数数量,如果不满足,则缓存当前的参数,否则,把多次调用 curry 函数的参数传入原始函数执行。

中间的 _curryN 是个挺有用的工具,可以将一个接受多个参数的函数转化为接受部分参数的 curry 函数,将其进一步分离,并优化代码结构如下:

js
var curryN = (fn, length) => {
  //对原始函数的包装,合并多次调用的柯里化的函数的参数,作为原始函数 fn 的参数,调用 fn
  var _warpFunc =
    (fn, args) =>
    (...arguments) =>
      fn.apply(null, args.concat(Array.prototype.slice.call(arguments)));

  return function () {
    var args = Array.prototype.slice.call(arguments);
    if (args.length < length) {
      return curryN(_warpFunc(fn, args), length - args.length);
    }
    return fn.apply(null, args);
  };
};

var curry = (fn, length = fn.length) => curryN(fn, length);

反柯里化 uncurry

反柯里化(uncurry)从字面上就可以看出和柯里化(curry)的含义正好相反,如果说柯里化的作用是固定部分参数,使函数针对性更强,那么反柯里化的作用就是扩大一个函数的应用范围,使一个函数适用于其他的对象。

如果说 curry 是预先传入一些参数,那么 uncurry 就是把原来已经固定的参数或者 this 上下文当作参数延迟来传递,也就是把 this.method 的调用模式转化成 method(this,arg1,arg2....),目的是创建一个更普适性的函数,可以被不同的对象使用。

比如,Array 上有一个 push 的方法,想让 push 这个函数不仅仅支持数组,还能够被其他对象使用:push(obj,args) ,如下:

js
var arr = [1, 2, 3];
arr.push(4);
// [1, 2, 3, 4]

var push = Array.prototype.push.unCurry();
var obj = {};
push(obj, 'a');
// Object {0: "a", length: 1}

javascript 里面,很多函数都不做对象的类型检测,而是只关心这些对象能做什么,如 ArrayStringprototype 上的方法就被特意设计成了这种模式,这些方法不对 this 的数据类型做任何校验,因此 obj 可以冒用 Arraypush 方法进行操作。这里再看一个 String 的例子:

js
var toUpperCase = String.prototype.toUpperCase.unCurry();
toUpperCase('js');
// JS

call 方法也可以被 unCurry

js
var a = {
  name: 'a',
  print: function () {
    console.log(this.name);
  },
  change: function (name) {
    this.name = name;
    console.log(this.name);
  },
};

a.print();
// a

var b = {
  name: 'b',
};

a.print.call(b);
// b

var call = Function.prototype.call.unCurry();
call(a.print, b);
// b

call(a.print, b, 'bb');
a.name;
// "aa"

b.name;
// "bb"

unCurry 本身也是方法,它也可以被反柯里化:

js
var unCurry = Function.prototype.unCurry.unCurry();
var toUpperCase = unCurry(String.prototype.toUpperCase);
toUpperCase('js');
// JS

反柯里化的实现

实现的代码很简单,只有几行,但是比较绕:

js
/**

- 为 Function 原型添加 unCurry 方法,这样所有的 function 都可以被修改适用范围;
- 需要返回一个修改后的适用范围的函数,此时需要借用 call 方法实现
- 但是需要处理参数,此时借用 apply 传入参数
*/

Function.prototype.unCurry = function () {
  // _fn 在本例中是 Array.prototype.push
  var _fn = this;
  return function () {
    // 这里有点绕,做个说明:
    // return Function.prototype.call.apply(_fn, arguments);
    //
    // 等价于:
    // var fCall = Function.prototype.call;
    // return fCall.apply(_fn, arguments);
    //
    // 等价于:
    // return _fn.fCall(...arguments);
    //
    // 等价于:
    // return _fn.apply(arguments[0], [].slice.call(arguments, 1));
    //
    // 即修改 _fn 中的 this 指向第一个参数,在本例中是 obj,
    // 剩下的参数传入原函数 _fn
    return Function.prototype.call.apply(_fn, arguments);
  };
};

柯里化扩展

实现 add(1)(2, 3)(4)(5) = 15 的效果。 很多人这里就犯嘀咕了:我怎么知道执行的时机? 其实,这里有个忍者技艺:valueOftoStringjs 在获取当前变量值的时候,会根据语境,隐式调用 valueOftoString 方法进行获取需要的值。

js
function currying(fn) {
  var allArgs = [];

  function next() {
    var args = [].slice.call(arguments);
    allArgs = allArgs.concat(args);
    return next;
  }
  // 字符类型
  next.toString = function () {
    return fn.apply(null, allArgs);
  };
  // 数值类型
  next.valueOf = function () {
    return fn.apply(null, allArgs);
  };

  return next;
}
var add = currying(function () {
  var sum = 0;
  for (var i = 0; i < arguments.length; i++) {
    sum += arguments[i];
  }
  return sum;
});