理解JS里的偏函数与柯里化

  联系到上篇博客讲的bind完整的语法为:

let bound = func.bind(context, arg1, arg2, ...);

  可以绑定上下文this和函数的初始参数。举例,我们有个乘法函数mul(a,b):

function mul(a, b) {
  return a * b;
}

  我们可以在该函数的基础上使用绑定创建一个double函数:

let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6

  调用mul.bind(null, 2)创建新函数double,传递调用mul函数,固定第一个参数上下文为null,第二个参数为2,多个参数传递也是如此。

  这称为偏函数应用——我们创造一个新函数,让现有的一些参数值固定。

  注意,这里确实不用this,但bind需要,所以必须使用null。

  为什么我们通常使用偏函数?

  这里我们偏函数的好处是:

  (1)通过创建一个名称易懂的独立函数(double,triple等),调用时无需每次传入第一个参数,因为第一个参数通过bind提供了固定值

  (2)另一种使用偏函数情况是,当我们有一个很通用的函数,为了方便提供一个较常用的变体。举例,我们有一个函数send(from, to, text),那么使用偏函数可以创建一个从当前用户发送的变体:sendTo(to, text)

偏函数与柯里化定义:

  维基百科中对偏函数 (Partial application) 的定义为:

In computer science, partial application (or partial function application) 
refers to the process of fixing a number of arguments to a function,

producing another function of smaller arity.

  翻译成中文:在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。(什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。)

  维基百科中对柯里化 (Currying) 的定义为:

In mathematics and computer science, 
currying is the technique of translating the evaluation of a function
that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions,

each with a single argument.

  翻译成中文:在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

  偏函数与柯里化区别:

  柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

  局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

使用没有上下文的偏函数

  bind可以实现偏函数应用,但是如果想固定一些参数,但不绑定this呢?

  内置的bind不允许这样,我们不能忽略上下文并跳转到参数。幸运的是,可以仅绑定参数partial函数容易实现。如下:

function partial(func, ...argsBound) {
  return function(...args) {
    return func.call(this, ...argsBound, ...args);
  }
}

let user = {
  firstName: "John",
  say(time, phrase) {
    alert(`[${time}] ${this.firstName}: ${phrase}!`);
  }
};

// 偏函数,绑定第一个参数,say的time
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
//调用新函数提供第二个参数phrase
user.sayNow("Hello");
// [10:00] Hello, John!

  调用partial(func[, arg1, arg2...])函数的结果为调用func的包装器(即第一个return的函数):

  (1)this一致(因为user.sayNow是通过user调用的)

  (2)然后给其...argsBound—— partial使用该参数("10:00")进行调用。

  (3)然后提供参数...args——提供给包装器的参数(“Hello“)

  所以使用spread运算符很容易实现。

柯里化实现

  有时人们混淆上面提及的偏函数和“柯里化”函数功能,柯里化是另一个有趣的处理函数技术。柯里化(Currying):转换一个调用函数f(a,b,c)f(a)(b)(c)方式调用。让我们实现柯里化函数,执行一个两元参数函数,即转换f(a,b)f(a)(b):

function curry(func) {
  return function(a) {
    return function(b) {
      return func(a, b);
    };
  };
}
// usage
function sum(a, b) {
  return a + b;
}

let carriedSum = curry(sum);
alert( carriedSum(1)(2) ); // 3

  上面是通过一系列包装器实现的。

  (1)curry(func)的结果是function(a)的一个包装器。

  (2)当调用sum(1)是,参数被保存在词法环境中,然后返回新的包装器function(b)

  (3)然后sum(1)(2)提供2并最终调用function(b),然后传递调用给原始多参数函数sum

高级柯里化实现

  有一些柯里化的高级实现,可以实现更复杂功能:其返回一个包装器,它允许函数提供全部参数被正常调用,或返回偏函数。实现如下:

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {//如果参数大于等于函数参数,那么允许函数提供全部参数被正常调用
      return func.apply(this, args);
    } else {//提供参数小于函数参数,返回偏函数
      return function pass(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };
}

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

let curriedSum = curry(sum);

// 提供全部参数,正常调用
alert( curriedSum(1, 2, 3) ); // 6

// 返回偏函数包装器,并提供2、3参数
alert( curriedSum(1)(2,3) ); // 6

  当我们运行时,有两个分支:

  1、提供全部参数正常调用:如果传递args数与原函数已经定义的参数个数一样或更长,那么直接调用。

  2、获得偏函数:否则,不调用func函数,返回另一个包装器,提供连接之前的参数一起做为新参数重新应用curried。然后再次执行一个新调用,返回一个新偏函数(如果参数不够)或最终结果。

  举例,让我们看sum(a, b, c)会怎样,三个参数,所以sum.length=3;

  如果调用curried(1)(2)(3):

  (1)第一次调用curried(1),在词法环境中记住1,返回包装器pass;

  (2)使用参数2调用包装器pass:其带着前面的参数1,连接他们然后调用curried(1,2),因为参数数量仍然小于3,返回包装器pass;

  (3)再次使用参数3调用包装器pass,带着之前的参数(1,2),然后增加3,并调用curried(1,2,3)——最终有三个参数,传递给原始函数,然后参数个数相等,就直接调用func函数。

总结

  1、当把已知函数的一些参数固定,结果函数被称为偏函数。通过使用bind获得偏函数,也有其他方式实现。

  用途:当我们不想一次一次重复相同的参数时,偏函数是很便捷的。如我们有send(from,to)函数,如果from总是相同的,可以使用偏函数简化调用。

  2、柯里化是转换函数调用从f(a,b,c)f(a)(b)(c),Javascript通常既实现正常调用,也实现参数数量不足时的偏函数方式调用。

  用途:(1)参数复用;(2)提前返回;(3)延迟计算或运行,参数随意设置。

  这里说一下“提前返回”,很常见的一个例子:兼容现代浏览器以及IE浏览器的事件添加方法。我们正常情况可能会这样写:

var addEvent = function(el, type, fn, capture) {
    if (window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    } 
};

  上面的方法有什么问题呢?很显然,我们每次使用addEvent为元素添加事件的时候,(eg. IE6/IE7)都会走一遍if...else if ...其实只要一次判定就可以了,怎么做?——柯里化。改为下面这样子的代码:

var addEvent = (function(){
    if (window.addEventListener) {
        return function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(el, sType, fn, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
})();

  初始addEvent的执行其实只实现了部分的应用(只有一次的if...else if...判定),而剩余的参数应用都是其返回函数实现的,典型的柯里化思想。

posted @ 2018-03-11 17:22 古兰精 阅读(...) 评论(...) 编辑 收藏