前端面试手写代码——JS函数柯里化
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函数调用后返回了checkPhone和checkEmail函数
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 总结和补充
- 柯里化突出一种重要思想:降低适用范围,提高适用性
- 柯里化的三个作用和特点:参数复用、提前返回、延迟执行
- 柯里化是闭包的一个典型应用,利用闭包形成了一个保存在内存中的作用域,把接收到的部分参数保存在这个作用域中,等待后续使用。并且返回一个新函数接收剩余参数
前端面试手写代码——JS函数柯里化的更多相关文章
- 前端面试手写代码——JS数组去重
目录 1 测试用例 2 JS 数组去重4大类型 2.1 元素比较型 2.1.1 双层 for 循环逐一比较(es5常用) 2.1.2 排序相邻比较 2.2 查找元素位置型 2.2.1 indexOf ...
- 前端面试手写代码——call、apply、bind
1 call.apply.bind 用法及对比 1.1 Function.prototype 三者都是Function原型上的方法,所有函数都能调用它们 Function.prototype.call ...
- 前端面试手写代码——模拟实现new运算符
目录 1 new 运算符简介 2 new 究竟干了什么事 3 模拟实现 new 运算符 4 补充 预备知识: 了解原型和原型链 了解this绑定 1 new 运算符简介 MDN文档:new 运算符创建 ...
- js函数柯里化,实现bind
1.柯里化: 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术. 举个栗子: 一个计算两数之和的函数,需要传递两个参数,柯里化 ...
- js函数柯里化
function curry(fn){ // 代码 } function add(a,b,c){ return a + b + c; } const execAdd = curry(add); exe ...
- 应用js函数柯里化currying 与ajax 局部刷新dom
直接上代码吧 最近读javascript核心概念及实践的代码 感觉很有用 备忘. <div id="request"></div> <script t ...
- 简单粗暴详细讲解javascript实现函数柯里化与反柯里化
函数柯里化(黑人问号脸)???Currying(黑人问号脸)???妥妥的中式翻译既视感:下面来一起看看究竟什么是函数柯里化: 维基百科的解释是:把接收多个参数的函数变换成接收一个单一参数(最初函数的第 ...
- 简单粗暴详细讲解javascript实现函数柯里化
函数柯里化(黑人问号脸)???Currying(黑人问号脸)???妥妥的中式翻译既视感:下面来一起看看究竟什么是函数柯里化: 维基百科的解释是:把接收多个参数的函数变换成接收一个单一参数(最初函数的第 ...
- 手写系列:call、apply、bind、函数柯里化
少废话,show my code call 原理都在注释里了 // 不覆盖原生call方法,起个别名叫myCall,接收this上下文context和参数params Function.prototy ...
随机推荐
- CodeForce-702C Cellular Network(查找)
Cellular Network CodeForces - 702C 给定 n (城市数量) 和 m (灯塔数量): 给定 a1~an 城市坐标: 给定 b1~bm 灯塔坐标: 求出灯塔照亮的最小半径 ...
- leetcode 盛水最多的容器 解析
采用双指针法: 主要思想:定义头尾两个指针,分别向中间遍历,当相遇时结束循环.存储每一次遍历容器盛水的最大容量,并不断更新. 盛水的最大容量为 左右指针高度的最小值 乘以 左右指针的距离即宽度. 则可 ...
- dede织梦会员模板调用template下模板head.htm方法及解析变量
1.找到dedecms会员中心的的目录 member ,然后在目录下用编辑器打开config.php 加入对dede模板解释函数如下: //php脚本开始 //引入arc.partview.cla ...
- ecshop 加入购物车和直接购买同时存在的方法
一.首先将直接购买的链接设置为 <a href="javascript:bool =1;addToCart({$goods.goods_id})"> bool值为1,g ...
- ecshop调用指定栏目下的文章的方法
打开 index.php 添加 fun函数一个,需放在<php与?>中间. /** * 获得指定栏目的文章列表. * @param int $cid 栏目ID * @param int $ ...
- Spring Boot中如何配置线程池拒绝策略,妥善处理好溢出的任务
通过之前三篇关于Spring Boot异步任务实现的博文,我们分别学会了用@Async创建异步任务.为异步任务配置线程池.使用多个线程池隔离不同的异步任务.今天这篇,我们继续对上面的知识进行完善和优化 ...
- 创建一个新的解耦的Orchard Core CMS网站
引言本文将介绍创建一个功能齐全.解耦的CMS网站的过程,该网站允许您编辑博客帖子并呈现它们.解耦是一种开发模型,其中站点的前端和后端(管理)托管在同一个Web应用程序中,但只有后端由CMS驱动.然后, ...
- Shell条件判断(6)- 多重条件判断
多重条件判断 多个条件判断一起使用 测试选项 作用 判断1 -a 判断2 逻辑与,判断1和判断2都成立,最终的结果才为真 判断1 -o 判断2 逻辑或,判断1和判断2有一个成立,最终的结果就为真 ! ...
- Linux C语言 取得MTU (最大传输单元)
参照这篇博客: http://www.geekpage.jp/programming/linux-network/book/04/4-21.php * 查看主机当前网卡,哪块在使用. ifconfig ...
- linux 上添加多个jdk
1. 首先将你需要上传的jdk 上传并解压 2.你可以自定义解压的路径 3. alternatives --install /usr/bin/java java /usr/java/jdk1.7.0_ ...