JavaScript函数柯里化

编辑: admin 分类: javascript 发布时间: 2021-11-17 来源:互联网
目录
  • 1 什么是函数柯里化
  • 2 柯里化的作用和特点
    • 2.1 参数复用
    • 2.2 提前返回
    • 2.3 延迟执行
  • 3 封装通用柯里化工具函数#
    • 4 总结和补充

      1 什么是函数柯里化

      在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名的。

      什么意思?简单来说,柯里化是一项技术,它用来改造多参数的函数。

      比如:

      // 这是一个接受3个参数的函数
      const add = function(x, y, z) {
        return x + y + z
      }
      
      
      

      我们将它变换一下,可以得到这样一个函数:

      // 接收一个单一参数
      const curryingAdd = function(x) {
        // 并且返回接受余下的参数的函数
        return function(y, z) {
          return x + y + z
        }
      }
      
      
      

      这样有什么区别呢?从调用上来对比:

      // 调用add
      add(1, 2, 3)
       
      // 调用curryingAdd
      curryingAdd(1)(2, 3)
      // 看得更清楚一点,等价于下面
      const fn = curryingAdd(1)
      fn(2, 3)
      
      

      可以看到,变换后的的函数可以分批次接受参数,先记住这一点,下面会讲用处。甚至fn(curryingAdd返回的函数)还可以继续变换

      如下:

      const curryingAdd = function(x) {
        return function(y) {
          return function(z) {
            return x + y + z
          }
        }
      }
      // 调用
      curryingAdd(1)(2)(3)
      // 即
      const fn = curryingAdd(1)
      const fn1 = fn(2)
      fn1(3)
      
      

      上面的两次变换过程,就是函数柯里化。

      简单讲就是把一个多参数的函数f,变换成接受部分参数的函数g,并且这个函数g会返回一个函数h,函数h用来接受其他参数。函数h可以继续柯里化。就是一个套娃的过程~

      那么费这么大劲将函数柯里化有什么用呢?

      2 柯里化的作用和特点

      2.1 参数复用

      工作中会遇到的需求:通过正则校验电话号、邮箱、身份证是否合法等等

      于是我们会封装一个校验函数如下:

      /**
       * @description 通过正则校验字符串
       * @param {RegExp} regExp 正则对象
       * @param {String} str 待校验字符串
       * @return {Boolean} 是否通过校验
       */
      function checkByRegExp(regExp, str) {
          return regExp.test(str)
      }
      
      

      假如我们要校验很多手机号、邮箱,我们就会这样调用:

      // 校验手机号
      checkByRegExp(/^1\d{10}$/, '15152525634'); 
      checkByRegExp(/^1\d{10}$/, '13456574566'); 
      checkByRegExp(/^1\d{10}$/, '18123787385'); 
      // 校验邮箱
      checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fsds@163.com'); 
      checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fdsf@qq.com'); 
      checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fjks@qq.com');
      
      

      貌似没什么问题,事实上还有改进的空间

      • 校验同一类型的数据时,相同的正则我们写了很多次。
      • 代码可读性较差,如果没有注释,我们并不能一下就看出来正则的作用

      我们试着使用函数柯里化来改进:

      // 将函数柯里化
      function checkByRegExp(regExp) {
          return function(str) {
              return regExp.test(str)
          }
      }
      
      
      

      于是我们传入不同的正则对象,就可以得到功能不同的函数:

      // 校验手机
      const checkPhone = curryingCheckByRegExp(/^1\d{10}$/)
      // 校验邮箱
      const checkEmail = curryingCheckByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)
      
      
      

      现在校验手机、邮箱的代码就简单了,并且可读性也增强了

      // 校验手机号
      checkPhone('15152525634'); 
      checkPhone('13456574566'); 
      checkPhone('18123787385'); 
      // 校验邮箱
      checkEmail('fsds@163.com'); 
      checkEmail('fdsf@qq.com'); 
      checkEmail('fjks@qq.com');
      
      

      这就是参数复用:我们只需将第一个参数regExp复用,就可以直接调用有特定功能的函数

      通用函数(如checkByRegExp)解决了兼容性问题,但也会带来使用的不便,比如不同的应用场景需要传递多个不同的参数来解决问题

      有的时候同一种规则可能会反复使用(比如校验手机的参数),这就造成了代码的重复,利用柯里化就能够消除重复,达到复用参数的目的。

      柯里化的一种重要思想:降低适用范围,提高适用性

      2.2 提前返回

      JS DOM事件监听程序中,我们用addEventListener方法为元素添加事件处理程序,但是部分浏览器版本不支持此方法,我日本cn2服务器http://www.558idc.com/jap.html们会使用attachEvent方法来替代。

      这时我们会写一个兼容各浏览器版本的代码:

      /**
       * @description: 
       * @param {object} element DOM元素对象
       * @param {string} type 事件类型
       * @param {Function} fn 事件处理函数
       * @param {boolean} isCapture 是否捕获
       * @return {void}
       */
      function addEvent(element, type, fn, isCapture) {
          if (window.addEventListener) {
              element.addEventListener(type, fn, isCapture)
          } else if (window.attachEvent) {
              element.attachEvent("on" + type, fn)
          }
      }
      
      

      我们用addEvent来添加事件监听,但是每次调用此方法时,都会进行一次判断,事实上浏览器版本确定下来后,没有必要进行重复判断。

      柯里化处理:

      function curryingAddEvent() {
          if (window.addEventListener) {
              return function(element, type, fn, isCapture) {
                  element.addEventListener(type, fn, isCapture)
              }
          } else if (window.attachEvent) {
              return function(element, type, fn) {
                  element.attachEvent("on" + type, fn)
              }
          }
      }
      const addEvent = curryingAddEvent()
       
      // 也可以用立即执行函数将上述代码合并
      const addEvent = (function curryingAddEvent() {
        ...
      })()
      
      

      现在我们得到的addEvent是经过判断后得到的函数,以后调用就不用重复判断了。

      这就是提前返回或者说提前确认,函数柯里化后可以提前处理部分任务,返回一个函数处理其他任务

      另外,我们可以看到,curryingAddEvent好像并没有接受参数。这是因为原函数的条件(即浏览器的版本是否支持addEventListener)是直接从全局获取的。

      逻辑上其实是可以改成:

      let mode = window.addEventListener ? 0 : 1;
      function addEvent(mode, element, type, fn, isCapture) {
        if (mode === 0) {
          element.addEventListener(type, fn, isCapture);
        } else if (mode === 1) {
          element.attachEvent("on" + type, fn);
        }
      }
      // 这样柯里化后就可以先接受一个参数了
      function curryingAddEvent(mode) {
          if (mode === 0) {
              return function(element, type, fn, isCapture) {
                  element.addEventListener(type, fn, isCapture)
              }
          } else if (mode === 1) {
              return function(element, type, fn) {
                  element.attachEvent("on" + type, fn)
              }
          }
      }
      
      

      当然没必要这么改~

      2.3 延迟执行

      事实上,上述正则校验和事件监听的例子中已经体现了延迟执行。

      curryingCheckByRegExp函数调用后返回了checkPhonecheckEmail函数

      curringAddEvent函数调用后返回了addEvent函数

      返回的函数都不会立即执行,而是等待调用。

      3 封装通用柯里化工具函数#

      上面我们对函数进行柯里化都是手动修改了原函数,将add改成了curryingAdd、将checkByRegExp改成了curryingCheckByRegExp、将addEvent改成了curryingAddEvent

      难道我们每次对函数进行柯里化都要手动修改底层函数吗?当然不是

      我们可以封装一个通用柯里化工具函数(面试手写代码)

      /**
       * @description: 将函数柯里化的工具函数
       * @param {Function} fn 待柯里化的函数
       * @param {array} args 已经接收的参数列表
       * @return {Function}
       */
      const currying = function(fn, ...args) {
          // fn需要的参数个数
          const len = fn.length
          // 返回一个函数接收剩余参数
          return function (...params) {
              // 拼接已经接收和新接收的参数列表
              let _args = [...args, ...params]
              // 如果已经接收的参数个数还不够,继续返回一个新函数接收剩余参数
              if (_args.length < len) {
                  return currying.call(this, fn, ..._args)
              }
             // 参数全部接收完调用原函数
              return fn.apply(this, _args)
          }
      }
      
      

      这个柯里化工具函数用来接收部分参数,然后返回一个新函数等待接收剩余参数,递归直到接收到全部所需参数,然后通过apply调用原函数。

      现在我们基本不用手动修改原函数来将函数柯里化了

      // 直接用工具函数返回校验手机、邮箱的函数
      const checkPhone = currying(checkByRegExp(/^1\d{10}$/))
      const checkEmail = currying(checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/))
      
      
      

      但是上面事件监听的例子就不能用这个工具函数进行柯里化了,原因前面说了,因为它的条件直接从全局获取了,所以比较特殊,改成从外部传入条件,就能用工具函数柯里化了。当然没这个必要,直接修改原函数更直接、可读性更强

      4 总结和补充

      • 柯里化突出一种重要思想:降低适用范围,提高适用性
      • 柯里化的三个作用和特点:参数复用、提前返回、延迟执行
      • 柯里化是闭包的一个典型应用,利用闭包形成了一个保存在内存中的作用域,把接收到的部分参数保存在这个作用域中,等待后续使用。并且返回一个新函数接收剩余参数

      到此这篇关于JavaScript函数柯里化的文章就介绍到这了,更多相关函数柯里化内容请搜索hwidc以前的文章或继续浏览下面的相关文章希望大家以后多多支持hwidc!