JavaScript函数柯里化


Posted in Javascript onNovember 07, 2021

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方法为元素添加事件处理程序,但是部分浏览器版本不支持此方法,我们会使用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函数柯里化的文章就介绍到这了,更多相关函数柯里化内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
jQuery 学习第五课 Ajax 使用说明
May 17 Javascript
JQuery从头学起第二讲
Jul 04 Javascript
jQuery插件datalist实现很好看的input下拉列表
Jul 14 Javascript
JS实现iframe自适应高度的方法示例
Jan 07 Javascript
JavaScript实现实时更新系统时间的实例代码
Apr 04 Javascript
Kotlin学习第一步 kotlin语法特性
May 25 Javascript
label+input实现按钮开关切换效果的实例
Aug 16 Javascript
vue、react等单页面项目应该这样子部署到服务器
Jan 03 Javascript
详解Angular Karma测试的持续集成实践
Nov 15 Javascript
JS获取当前时间戳方法解析
Aug 29 Javascript
npm ci命令的基本使用方法
Sep 20 Javascript
原生JavaScript实现拖动校验功能
Sep 29 Javascript
JS数组去重详情
Nov 07 #Javascript
手写实现JS中的new
Nov 07 #Javascript
用JS写一个发布订阅模式
Nov 07 #Javascript
浅谈JavaScript浅拷贝和深拷贝
JavaScript严格模式不支持八进制的问题讲解
Javascript使用integrity属性进行安全验证
Nov 07 #Javascript
JavaScript中时间格式化新思路toLocaleString()
Nov 07 #Javascript
You might like
php安全配置 如何配置使其更安全
2011/12/16 PHP
Linux系统下PHP-FPM的安装和配置教程
2015/08/17 PHP
阻止JavaScript事件冒泡传递(cancelBubble 、stopPropagation)
2007/05/08 Javascript
jquery Firefox3.5中操作select的问题
2009/07/10 Javascript
jQuery新闻滚动插件 jquery.roller.js
2011/06/27 Javascript
JQuery 自定义CircleAnimation,Animate方法学习笔记
2011/07/10 Javascript
jquery特效 幻灯片效果示例代码
2013/07/16 Javascript
javascript ajax 仿百度分页函数
2013/10/29 Javascript
Javascript使用post方法提交数据实例
2015/08/03 Javascript
以Python代码实例展示kNN算法的实际运用
2015/10/26 Javascript
深入理解Node.js的HTTP模块
2016/10/12 Javascript
微信小程序实现图片上传功能实例(前端+PHP后端)
2018/01/10 Javascript
JS同步、异步、延迟加载的方法
2018/05/05 Javascript
JavaScript对象的浅拷贝与深拷贝实例分析
2018/07/25 Javascript
vue组件中的样式属性scoped实例详解
2018/10/30 Javascript
详解项目升级到vue-cli3的正确姿势
2019/01/28 Javascript
jQuery实现弹出层效果
2019/12/10 jQuery
记录微信小程序 height: calc(xx - xx);无效问题
2019/12/30 Javascript
Python实现的监测服务器硬盘使用率脚本分享
2014/11/07 Python
Anaconda下配置python+opencv+contribx的实例讲解
2018/08/06 Python
Python将string转换到float的实例方法
2019/07/29 Python
基于Django的乐观锁与悲观锁解决订单并发问题详解
2019/07/31 Python
基于Python实现大文件分割和命名脚本过程解析
2019/09/29 Python
pandas将多个dataframe以多个sheet的形式保存到一个excel文件中
2019/10/10 Python
html5 canvas绘制矩形和圆形的实例代码
2016/06/16 HTML / CSS
乌克兰第一的珠宝网上商店:Gold.ua
2019/11/29 全球购物
大学自荐信
2013/12/12 职场文书
关于期中考试的反思
2014/02/02 职场文书
善意的谎言事例
2014/02/15 职场文书
2014年元旦感言
2014/03/06 职场文书
班长演讲稿范文
2014/04/24 职场文书
警示教育活动总结
2014/05/05 职场文书
妇联领导班子剖析材料
2014/08/21 职场文书
2014年保密工作总结
2014/11/22 职场文书
2015年护士工作总结范文
2015/03/31 职场文书
Vue OpenLayer 为地图绘制风场效果
2022/04/24 Vue.js