乐趣区

关于javascript:前端面试手写代码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)

能够看到,变换后的的函数能够分批次承受参数,先记住这一点,上面会讲用途。甚至 fncurryingAdd 返回的函数)还能够持续 变换

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');

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

  1. 校验同一类型的数据时,雷同的正则咱们写了很屡次。
  2. 代码可读性较差,如果没有正文,咱们并不能一下就看进去正则的作用

咱们试着应用函数柯里化来改良:

// 将函数柯里化
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 总结和补充

  1. 柯里化突出一种重要思维:升高适用范围,进步适用性
  2. 柯里化的三个作用和特点:参数复用、提前返回、提早执行
  3. 柯里化是 闭包 的一个典型利用,利用闭包造成了一个保留在内存中的作用域,把接管到的局部参数保留在这个作用域中,期待后续应用。并且返回一个新函数接管残余参数

公众号【前端嘛】获取更多前端优质内容

退出移动版