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 相关文章推荐
javascript学习(一)构建自己的JS库
Jan 02 Javascript
js open() 与showModalDialog()方法使用介绍
Sep 10 Javascript
JavaScript italics方法入门实例(把字符串显示为斜体)
Oct 17 Javascript
jQuery中find()方法用法实例
Jan 07 Javascript
JavaScript——DOM操作——Window.document对象详解
Jul 14 Javascript
AngularJS入门教程之双向绑定详解
Aug 18 Javascript
jQuery实现的浮动层div浏览器居中显示效果
Feb 03 Javascript
vue-router项目实战总结篇
Feb 11 Javascript
原生JS实现的碰撞检测功能示例
May 18 Javascript
前端js中的事件循环eventloop机制详解
May 15 Javascript
修改layui的后台模板的左侧导航栏可以伸缩的方法
Sep 10 Javascript
VSCode Vue开发推荐插件和VSCode快捷键(小结)
Aug 08 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/06/09 PHP
详解php与ethereum客户端交互
2018/04/28 PHP
仅img元素创建后不添加到文档中会执行onload事件的解决方法
2011/07/31 Javascript
简单实用的反馈表单无刷新提交带验证
2013/11/15 Javascript
js 剪切板的用法(clipboardData.setData)与js match函数介绍
2013/11/19 Javascript
解决js中window.open弹出的是上次的缓存页面问题
2013/12/29 Javascript
sogou地图API用法实例教程
2014/09/11 Javascript
JavaScript获取图片真实大小代码实例
2014/09/24 Javascript
分享20个提升网站界面体验的jQuery插件
2014/12/15 Javascript
jQuery选择器源码解读(六):Sizzle选择器匹配逻辑分析
2015/03/31 Javascript
asp.net中oracle 存储过程(图文)
2015/08/12 Javascript
基于JS实现简单的样式切换效果代码
2015/09/04 Javascript
jQuery 的 ready()的纯js替代方法
2016/11/20 Javascript
实现点击下箭头变上箭头来回切换的两种方法【推荐】
2016/12/14 Javascript
使用vs code开发Nodejs程序的使用方法
2017/09/21 NodeJs
浅谈webpack打包之后的文件过大的解决方法
2018/03/07 Javascript
layui复选框限制选择个数的方法
2019/09/18 Javascript
Vue中使用matomo进行访问流量统计的实现
2019/11/05 Javascript
小程序跳转H5页面的方法步骤
2020/03/06 Javascript
js、jquery实现列表模糊搜索功能过程解析
2020/03/27 jQuery
JS+canvas五子棋人机对战实现步骤详解
2020/06/04 Javascript
Vue表单提交点击事件只允许点击一次的实例
2020/10/23 Javascript
vue实现两个组件之间数据共享和修改操作
2020/11/12 Javascript
python类和函数中使用静态变量的方法
2015/05/09 Python
详解Python中的日志模块logging
2015/06/19 Python
10款最好的Python开发编辑器
2019/07/03 Python
pygame实现俄罗斯方块游戏(对战篇1)
2019/10/29 Python
分享30个新鲜的CSS3打造的精美绚丽效果(附演示下载)
2012/12/28 HTML / CSS
Roxy美国官网:澳大利亚冲浪、滑雪健身品牌
2016/07/30 全球购物
Foot Locker意大利官网:全球领先的运动鞋和服装零售商
2017/05/30 全球购物
欧洲领先的电子和电信零售商和服务提供商:Currys PC World Business
2017/12/05 全球购物
优秀英语专业毕业生求职信
2013/11/23 职场文书
人力资源本科毕业生求职信
2014/06/04 职场文书
上班迟到检讨书300字
2014/10/18 职场文书
唐山大地震的观后感
2015/06/05 职场文书
离职证明范本
2015/06/12 职场文书