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当中的代码嗅探扩展原生对象和原型(prototype)
Jan 11 Javascript
删除javascript中注释语句的正则表达式
Jun 11 Javascript
基于jQuery实现点击弹出层实例代码
Jan 01 Javascript
JavaScript轻松创建级联函数的方法示例
Feb 10 Javascript
Javascript封装id、class与元素选择器方法示例
Mar 13 Javascript
js表单序列化判断空值的实例
Sep 22 Javascript
Angularjs中date过滤器失效的问题及解决方法
Jul 06 Javascript
微信小程序实现默认第一个选中变色效果
Jul 17 Javascript
详解性能更优越的小程序图片懒加载方式
Jul 18 Javascript
Vue组件通信入门之Provide和Inject机制
Dec 29 Javascript
JavaScript onclick事件使用方法详解
May 15 Javascript
addEventListener()和removeEventListener()追加事件和删除追加事件
Dec 04 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
Windows下安装Memcached的步骤说明
2010/04/25 PHP
PHP加密解密字符串汇总
2015/04/26 PHP
php 利用array_slice函数获取随机数组或前几条数据
2015/09/30 PHP
PHP批量删除jQuery操作
2017/07/23 PHP
基于CI(CodeIgniter)框架实现购物车功能的方法
2018/04/09 PHP
PHP使Laravel为JSON REST API返回自定义错误的问题
2018/10/16 PHP
Javascript UrlDecode函数代码
2010/01/09 Javascript
基于jquery实现的文字向上跑动类似跑马灯的效果
2014/06/17 Javascript
JS+CSS实现可拖动的弹出提示框
2015/02/16 Javascript
TypeError document.getElementById(...) is null错误原因
2015/05/18 Javascript
易被忽视的js事件问题总结
2016/05/14 Javascript
jquery 无限极下拉菜单的简单实例(精简浓缩版)
2016/05/31 Javascript
Bootstrap基本布局实现方法详解
2016/11/25 Javascript
JS实现前端缓存的方法
2017/09/21 Javascript
通过vue-router懒加载解决首次加载时资源过多导致的速度缓慢问题
2018/04/08 Javascript
7个好用的JavaScript技巧分享(译)
2019/05/07 Javascript
简单了解Ajax表单序列化的实现方法
2019/06/14 Javascript
实用的 vue tags 创建缓存导航的过程实现
2020/12/03 Vue.js
[01:57]2016完美“圣”典风云人物:国士无双专访
2016/12/04 DOTA
Python常用的日期时间处理方法示例
2015/02/08 Python
Python基于Pymssql模块实现连接SQL Server数据库的方法详解
2017/07/20 Python
Python中输入和输出(打印)数据实例方法
2019/10/13 Python
python数据预处理 :数据共线性处理详解
2020/02/24 Python
Keras - GPU ID 和显存占用设定步骤
2020/06/22 Python
SISLEY希思黎官方旗舰店:享誉全球的奢华植物美容品牌
2018/04/25 全球购物
绿色美容,有机护肤品和化妆品:Safe & Chic
2018/10/29 全球购物
伦敦剧院门票:London Theatre Direct
2018/11/21 全球购物
西班牙购买隐形眼镜、眼镜和太阳镜网站:Lentiamo.es
2020/06/11 全球购物
生产车间主管岗位职责
2013/12/28 职场文书
英语商务邀请函范文
2014/01/16 职场文书
家居装修公司创业计划书范文
2014/03/20 职场文书
会计专业自荐信
2014/06/03 职场文书
总经理岗位职责说明书
2014/07/30 职场文书
大学生社会实践感想
2015/08/11 职场文书
毕业设计工作总结
2015/08/14 职场文书
2016年幼儿园教师师德承诺书
2016/03/25 职场文书