• 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!

     

      

  • 相关阅读:
    计算机最小单位
    api接口调用
    STM32SystemInit函数
    关于 Verilog 的 TimeScale
    破获ARM64位CPU下linux crash要案之神技能:手动恢复函数调用栈
    芯片后仿
    破获ARM64位CPU下linux crash要案之神技能:手动恢复函数调用栈
    HardFault定位方法和步骤
    BSP和SDK的区别
    armCPSR寄存器
  • 原文地址:https://www.cnblogs.com/haoqiyouyu/p/16068375.html
Copyright © 2020-2023  润新知