18-柯里化函数/偏函数

几篇不错的文章:

目前的理解:偏函数是柯里化函数的一种形态。做了一些参数的固定。

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

柯里化其实本身是固定一个可以预期的参数,并返回一个特定的函数,处理批特定的需求。这增加了函数的适用性,但同时也降低了函数的适用范围。

通用函数:

function currying(fn) {
  // arguments为currying函数执行传入的参数
  var slice = Array.prototype.slice,
    // 去掉currying执行时传入的第一个参数->fn,拿到其他传入的所有参数
    __args = slice.call(arguments, 1);
  return function () {
    // 这个arguments是在执行这个匿名函数时,传入的参数
    var __inargs = slice.call(arguments);
    // 把执行currying执行时传入的fn执行,并把执行两次时收集的入参合并起来,作为fn的参数
    return fn.apply(null, __args.concat(__inargs));
  };
}

function func(...args) {
  return args.reduce((p, n) => p + n);
}

let f = currying(func, 1);
let res = f(2, 3, 4);
console.log(res);

1 提高适用性。

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

 看下面一个例子:

例子中,创建了一个map通用函数,用于适应不同的应用场景。显然,通用性不用怀疑。同时,例子中重复传入了相同的处理函数:square和dubble。

应用中这种可能会更多。当然,通用性的增强必然带来适用性的减弱。但是,我们依然可以在中间找到一种平衡。

我们利用柯里化改造一下:

// 柯里化通用方法
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 dubbleFn(x) {
  return (x *= 2);
}

function func(handler, data) {
  return data.map(handler);
}

let f = currying(func, dubbleFn);
let res = f([1, 2, 3, 4]);
console.log(res); // [ 2, 4, 6, 8 ]

由此,可知柯里化不仅仅是提高了代码的合理性,更重的它突出一种思想---降低适用范围,提高适用性。


 2.延迟执行 

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

var curryFn = function (fn) {
  var _args = [];
  return function cb() {
    // 递归结束条件
    if (arguments.length == 0) {
      return fn.apply(this, _args);
    }
    // 第一种写法 调用Array的push方法,借用apply,合并_args和传入的arguments
    // apply可以将数组型参数扩展成参数列表,这样合并两个数组就可以直接传数组参数了。
    /* 但是合并数组为什么不直接使用Array.prototype.concat()呢?
        -因为concat不会改变原数组,concat会返回新数组,而上面apply这种写法直接改变数组_args。 */

    Array.prototype.push.apply(_args, arguments);
    // 第二种写法 :这样写都能看得懂
    // _args.push(...arguments);
    // 返回他自己,是个递归
    return cb;
  };
};

function func(...args) {
  return args.reduce((p, n) => p + n);
}

let temp = curryFn(func);
console.log(temp(10)(1)(8)()); //19

3 固定易变因素(偏函数)

柯里化特性决定了它这应用场景。提前把易变因素,传参固定下来,生成一个更明确的应用函数。最典型的代表应用,是bind函数用以固定this这个易变对象。
Function.prototype.bind = function (context) {
  let _args = Array.prototype.slice.call(arguments, 1);
  return () => {
    let _inArgs = Array.prototype.slice.call(arguments);
    return this.apply(context, _args.concat(_inArgs));
  };
};

4.提前返回

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>柯里化函数-提前返回</title>
    <style>
      .box {
        width: 200px;
        height: 40px;
        margin-bottom: 50px;
        line-height: 40px;
        background: rebeccapurple;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <!-- normal -->
    <div id="box" class="box"><h1>box1</h1></div>
    <div id="box2" class="box"><h1>box2</h1></div>

    <!-- curring -->
    <div id="box3" class="box"><h1>box3</h1></div>
    <div id="box4" class="box"><h1>box4</h1></div>
  </body>
</html>
<script>
  var oBox = document.querySelector("#box");
  var oBox2 = document.querySelector("#box2");

  var oBox3 = document.querySelector("#box3");
  var oBox4 = document.querySelector("#box4");

  function clickFn() {
    console.log("hello~");
  }
  var addEvent = function (el, type, fn, capture) {
    console.log("我是normal");
    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(oBox, "click", clickFn, false);
  addEvent(oBox2, "click", clickFn, false);

  //   -------------------------------------------------------------

  var addEventCurring = (function () {
    if (window.addEventListener) {
      console.log("我被柯里化了");
      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) {
        el.attachEvent("on" + sType, function (e) {
          fn.call(el, e);
        });
      };
    }
  })();

  addEventCurring(oBox3, "click", clickFn, false);
  addEventCurring(oBox4, "click", clickFn, false);
</script>

运行后查看控制台可以发现:

通过运行结果我们发现,当我们的 addEvent 和 addEventCurring 都被执行了两次,前者会走入if判断两次,后者只会进入到一次if判断;这就是被柯里化后的效果,可以细细品味。


 

 高级柯里化实现

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

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)延迟计算或运行,参数随意设置。


 

 偏函数与柯里化区别:

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

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

使用没有上下文的偏函数

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

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

(这句话的意思是,我们在使用bind的时候,必须还得传递一个context上下文,我们封装一个方法模拟bind实现的偏函数,但是不用传入bind的第一个参数上下文)

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], John : hello!

 

 

  

posted @ 2022-03-28 18:04  猎奇游渔  阅读(154)  评论(0编辑  收藏  举报