秋招复习计划-JavaScript知识点10

  |  

前言

函数柯里化及其通用封装


40



什么是柯里化

柯里化:Currying,是把接受多个参数的函数变换成接受一个单一参数的函数,并返回接受余下的参数而且返回结果的新函数的技术

柯里化是函数的一个高级应用,我们知道接收函数为参数的函数,都可以叫做高阶函数.我们常常利用高阶函数来封装一些公共的逻辑。而柯里化就是高阶函数的一种特殊用法。

柯里化封装函数,就是对函数进行柯里化封装的函数。假设我们有一个柯里化封装函数createCurry,它接收函数A作为参数,运行后返回一个新的函数,并且这个新的函数能够处理A的剩余参数。

这些定义可能比较难理解,我们举例来说明

假设我们有这个一个函数A

1
2
3
function A(a, b, c) {
//do something
}

现在我们有一个柯里化封装函数createCurry。他接收一个函数为参数,返回这个函数的柯里化结果

1
let _A = createCurry(A);

那么_A作为柯里化后返回的函数,它能够处理A的剩余参数。所以,一下这些调用方式的发哦的结果都是一样的

1
2
3
4
5
A(1, 2, 3);
_A(1, 2, 3);
_A(1, 2)(3);
_A(1)(2, 3);
_A(1)(2)(3);

如何进行柯里化封装

人工封装

如何进行柯里化封装,及如何构造一个可以对函数进行柯里化封装的函数。一般来说我们都是借助柯里化通用式来得到柯里化封装函数的。 但在简单的场景下,也就是对一些简单的函数,我们可以凭借眼里人工封装。

例如这个简单的加法函数

1
2
3
function add(a, b, c) {
return a + b + c;
}

那么add函数的柯里化函数_add则如下

1
2
3
4
5
6
7
function _add(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}

下面的运算方式是等价的

1
2
add(1, 2, 3);
_add(1)(2)(3);

当然,靠人眼封装的柯里化封装函数自由度偏低,而且只适合简单场景。

初步封装

初步封装通过闭包的形式将需要柯里化的函数的第一个参数保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要curring的函数

1
2
3
4
5
6
7
let curring = function(fn) {
let args = Array.prototype.slice.call(arguments, 1);
return function() {
let newArgs = args.concat(Array.prototype.slice.call(arguments));
return fn.apply(this. newArgs);
}
}

但是这种初步封装是有缺陷的,这种封装方式只能扩展第一个参数。以之前的add函数为例

1
2
let _add = curring(add, a);
_add(b, c)

这个柯里化封装函数只能这样使用。

所以我们需要一种柯里化通用式。

柯里化通用式

1
2
3
4
5
6
7
8
9
10
11
12
function createCurry(func, args) {
let arity = func.length;
let args = args || [];
return function() {
let _args = [].slice.call(arguments);
[].push.apply(_args, args);
if(_args.length < arity) {
return createCurry.call(this, func, _args);
}
return func.apply(this, _args);
}
}

这个柯里化函数createCurry的封装借助了闭包与递归调用,实现了任意长度的参数手机,并在收集完毕后,执行函数并返回结果。虽然我们之前做了很多的讲解,但是这个柯西化封装函数还是很难理解,甚至对于这个柯里化封装函数怎么使用还很懵。没有关系,后面我们会举例说明。但是我的建议还是是在关键位置做好断点自己执行一遍,在每一步都看一下各个变量的值。

柯里化的作用

如果你看懂了之前的封装过程,你就会发现。将函数通过柯里化封装函数转换成一个柯里化函数,它得到的结果还是原先函数执行的结果啊。这个柯里化过程不是把函数执行的过程复杂化了吗。是的,柯里化封装的确把函数执行变得复杂了,但是它同时赋予了函数更高的自由度。而对于函数参数的自有处理,正是柯里化的核心所在。那么柯里化到底有什么作用呢?

参数复用

参数复用是函数柯里化最主要的作用,尤其是在项目开发的过程中对一些通用函数的封装。

在项目当中,验证客户输入的各种注册登录信息是个很常见的场景。例如我们像判断一下用户输入的一串数字是否是正确的手机号,按照正常的思路,我们会封装这样一个函数

1
2
3
function checkPhone(phoneNumber) {
return /^1[345678]\d{9}$/.test(phoneNumber);
}

如果我们又想验证一个邮箱呢。那么我们会继续封装一个函数

1
2
3
function checkEmail(email) {
return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
}

这样的应用场景还有很多,验证省份证号码、验证密码。所以为了统一逻辑,我们可能会封装一个更加通用的函数,将用于验证的正则与将要被验证的字符串作为参数传入

1
2
3
function check(targetString, reg) {
return reg.test(targetString);
}

但是使用这样的封装,在使用时就会有一点麻烦,因为我们在验证时总要输入一串正则,这样会降低使用时的效率。

1
2
check(/^1[34578]\d{9}$/, '14900000088');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');

铺垫了这么多,其实就是想说,借助柯里化,我们在check的基础上在做一层封装,就能解决这个问题

1
2
3
4
let _check = createCurry(check);

let checkPhone = _check(/^1[34578]\d{9}$/);
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

这里_check、checkPhone和checkEmail都是check的柯里化函数

那么在之后使用的时候,过程就变得简单直观了

1
2
checkPhone('183888888');
checkEmail('xxxxx@test.com');

这里的checkPhone和checkEmail其实就是通过柯里化完成了对正则表达式这个参数的复用。

那么这里柯里封装函数是如何完成对check函数的柯里化的呢,柯里化后的函数又是怎么执行的呢。现在我们就来分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createCurry(func, args) {
let arity = func.length;
let args = args || [];
return function() {
let _args = [].slice.call(arguments);
[].push.apply(_args, args);
if(_args.length < arity) {
return createCurry.call(this, func, _args);
}
return func.apply(this, _args);
}
}
function check(targetString, reg) {
return reg.test(targetString);
}
let _check = createCurry(check);
let checkPhone = _check(/^1[34578]\d{9}$/);
checkPhone('183888888');

首先,调用createCurry函数,将check函数作为参数传入。arity作为check的具名参数个数,值为2。args是默认值,值为一个空数组。返回一个函数,_check被赋值为这个函数。需要注意的是,由于返回的函数中有对this的引用,所以在这里形成了一个闭包,在闭包中保存arity和args变量。

然后,我们将一个正则表达式作为参数传入到_check函数中。_check函数将arguments类数组对象转变成数组,赋值给_args,这时数组里只有一个值,就是作为参数传入的正则表达式。将闭包中的args压入_args数组中,这里_args数组并没有改变,因为args是个空数组,所以_args.length为1,小于arity的值2。所以返回createCurry的递归调用。将_args作为参数传入。

递归的调用结果arity仍为2,args为传入的_args,是一个数组,数组中包含正则表达式。返回函数,由于函数中仍包含this的引用,所以这里又形成了一个闭包,闭包中有arity和args变量。返回的函数赋值给checkPhone。这里我们就完成了check的柯里化了。

当我们调用checkPhone函数,将要检查的字符串传入。将arguments转换为数组,赋值给_args,这是_args中只有要检查的字符串。我们将args压入_args数组中,也就是将之前保存在args中的正则表达式压入到_args中。这时候变量参数满足check函数调用所需要的值。返回func的调用结果。

这就是整个柯里化封装和柯里化函数调用的过程。

提前确认

1
2
3
4
5
6
7
8
9
10
11
var on = function(element, event, handler) {
if(document.addEventListener) {
if(element && event && handler) {
element.addEventListener(event, handler, false);
}
} else {
if(element && event && handler) {
element.attachEvent('on' + event, handler);
}
}
}

这种方式比较常见,但是这样会导致每次绑定都会进行一次判断影响性能。所以我们可以使用立即执行函数来做优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var on = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();

但是我还是不知道为什么这算作函数柯里化,我感觉它并不符合函数柯里化的定义

延迟运行

js中经常使用的bind,从原理上来说,使用的就是函数柯里化的初步封装。

1
2
3
4
5
6
7
Function.prototype.bind = function(context) {
var _this = this;
var args = Array.prototype.slice.call(arguments, 1);
return function() {
return _this.apply(context, args);
}
}

函数柯里化过程中的性能问题

函数柯里化虽然能够提升函数的灵活性,但是他也会带来一些性能上的问题。最主要的就是这四个方面:

  • 存取arguments对象通常比存取命名参数要慢一些
  • 一些老版本的浏览器在获取arguments.length的时候相当的慢
  • 使用fn.apply()和fn.call()要比直接调用fn()慢些
  • 创建大量的嵌套作用域和闭包函数会带来空间上的额外开销

但是在大多数项目中,主要的性能瓶颈在于DOM节点的操作。而且对比函数柯西化带来的好处,这些JS上的性能损耗几乎可以忽略不记,所以在项目中,可以放心使用柯西化封装函数对函数进行封装。

最后来看一道面试题

这是一道无限参数的柯里化

1
2
3
4
实现这样一个add方法,使计算结果能够满足对于任意数量的参数返回它们的和
add(1)(2) = 3;
add(1)(3)(5)(7)(9) = 25
add(1,2,3,4)(5)(6,7) = 28

先直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function add() {
//第一次执行时,定义一个数组专门存放所有参数
var _args = [].sclice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var adder = function() {
var _adder = function() {
_args.push(...arguments);
return _adder;
}
// 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function() {
return _args.reduce(function(a, b) {
return a + b;
});
}
return _adder;
}
return adder(...args);
}

这道题最大的难点在于我们不知道函数会执行几次,也就不能用之前的柯西化通用式来进行柯西化转换,只能人工封装。

这个方法在我看来用来一个取巧的方法。不管运行几次,我都返回一个函数,让他能继续运行下一次,但是如果我们要输出结果怎么办。所以它修改了返回函数的toString方法,让他计算闭包中变量数组的和,而它的返回的函数的作用只是把每次调用的参数压入那个数组。

我们来看一下运行结果。

add

这个运行结果数字前的f就是这种方法的最直接的反应。

当我们第一次运行的时候,创建一个专门存放数组的所有参数。然后定义一个adder函数。这个函数的作用在于形成一个闭包,让其内部的_adder函数能够保持对_args的访问。然后定义_adder函数,这个函数的作用就是将它的所有参数压入_args数组中,然后返回自身。接着定义_adder函数发生隐式转换的时候操作。然后返回_adder函数。

所以add(1)最后输出的其实就是_adder,然后因为_adder的隐式转换,所以输出的是1。

文章目录
  1. 1. 什么是柯里化
  2. 2. 如何进行柯里化封装
    1. 2.1. 人工封装
    2. 2.2. 初步封装
    3. 2.3. 柯里化通用式
  3. 3. 柯里化的作用
    1. 3.1. 参数复用
    2. 3.2. 提前确认
    3. 3.3. 延迟运行
  4. 4. 函数柯里化过程中的性能问题
  5. 5. 最后来看一道面试题
|