2021-11-03 10:05:10 +08:00

318 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 函数柯里化
## 经典真题
- 什么是函数柯里化?
## 什么是函数柯里化
在计算机科学中,柯里化(英语:*Currying*),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的,尽管它是 *Moses Schönfinkel* 和戈特洛布·弗雷格发明的。
在直觉上,柯里化声称如果你固定某些参数,你将得到接受余下参数的一个函数。
我们姑且叫它返回函数,在调用返回函数的时候,它将判断当前的参数和之前被柯里化函数固定的参数拼起来之后,是否达到了原本函数的参数个数。
如果是,则执行原本的函数,得到结果;如果没有达到,则要继续调用柯里化函数来固定目前的参数。
在理论计算机科学中,柯里化提供了在简单的理论模型中,比如:只接受一个单一参数的 *lambda* 演算中,研究带有多个参数的函数的方式。
函数柯里化的对偶是*Uncurrying*,一种使用匿名单参数函数来实现多参数函数的方法。
## 柯里化快速入门
接下来,我们来通过一个简单的示例,让大家快速体会函数柯里化。
假设我们有一个求取两个数之和的函数:
```js
function add(x, y) {
return x + y;
}
console.log(add(1, 2)); // 3
console.log(add(5, 7)); // 12
```
在上面的示例中,我们有一个 *add* 函数,接收两个形参,返回两形参的和。
在调用的时候,我们每次也需要传递两个参数。
现在,我们对其进行柯里化,如下:
```js
function add(x) {
return function (y) {
return x + y;
}
}
console.log(add(1)(2)); // 3
console.log(add(5)(7)); // 3
```
在上面的代码中,我们对 *add* 函数进行了柯里化改造,只接受一个参数,但是返回的也不是值了,而是返回一个函数,这个函数也接收一个参数,然后利用闭包的特性,可以访问到最开始传入的 *x* 的值,最终返回 *x**y* 的和。
所以,通过上面的这个示例,我们能够体会到前面所说的柯里化函数的特点:
一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
## 函数柯里化实际应用
通过上面的例子,我们体验到了什么是柯里化函数。
但是问题来了,费这么大劲封装一层,到底有什么用处呢?
没有好处想让我们程序员多干事情是不可能滴,这辈子都不可能。
所以接下来我们就来看一下函数柯里化的一个实际应用。
**参数复用**
就是将相同的参数固定下来。
```js
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
// 即使是相同的正则表达式,也需要重新传递一次
console.log(check(/\d+/g, 'test1')); // true
console.log(check(/\d+/g, 'testtest')); // false
console.log(check(/[a-z]+/g, 'test')); // true
// Currying后
function curryingCheck(reg) {
return function (txt) {
return reg.test(txt)
}
}
// 正则表达式通过闭包保存了起来
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
console.log(hasNumber('test1')); // true
console.log(hasNumber('testtest')); // false
console.log(hasLetter('21212')); // false
```
上面的示例是一个正则的校验,正常来说直接调用 *check* 函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数 *reg* 进行复用,这样别的地方就能够直接调用 *hasNumber、hasLetter* 等函数,让参数能够复用,调用起来也更方便。
**提前确认**
```js
/**
*
* @param {要绑定事件的 DOM 元素} element
* @param {绑定什么事件} event
* @param {事件处理函数} handler
*/
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);
}
}
}
on(div, 'click', function(){})
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);
}
};
}
})();
on(div, 'click', function(){})
//换一种写法可能比较好理解一点,上面就是把 isSupport 这个参数给先确定下来了
var on = function (isSupport, element, event, handler) {
isSupport = isSupport || document.addEventListener;
if (isSupport) {
return element.addEventListener(event, handler, false);
} else {
return element.attachEvent('on' + event, handler);
}
}
on(true, div, 'click', function(){})
on(true, div, 'click', function(){})
on(true, div, 'click', function(){})
```
我们在做项目的过程中,封装一些 *DOM* 操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对于第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。
## 封装通用柯里化函数
接下来我们来封装一个通用的柯里化函数。
```js
function curry() {
var fn = arguments[0]; // 获取要执行的函数
var args = [].slice.call(arguments, 1); // 获取传递的参数,构成一个参数数组
// 如果传递的参数已经等于执行函数所需的参数数量
if (args.length === fn.length) {
return fn.apply(this, args)
}
// 参数不够向外界返回的函数
function _curry(){
// 推入之前判断
// 将新接收到的参数推入到参数数组中
args.push(...arguments);
if(args.length === fn.length){
return fn.apply(this, args)
}
return _curry;
}
return _curry;
}
```
对上面的代码进行测试:
```js
// 测试 1
function add(a, b, c) {
return a + b + c;
}
console.log(curry(add)(1)(2)(3)); // 6
console.log(curry(add, 1)(2)(3)); // 6
console.log(curry(add, 1, 2, 3)); // 6
console.log(curry(add, 1)(3, 4)); // 8
var addCurrying = curry(add)(2);
console.log(addCurrying(7)(8)); // 17
// 测试 2
function check(reg, txt) {
return reg.test(txt)
}
var hasNumber = curry(check)(/\d+/g);
console.log(hasNumber('test1'));// true
```
## 一道经典的柯里化面试题
实现一个 *add* 方法,使计算结果能够满足如下预期:
```js
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
```
要完成上面的需求,我们就可以使用柯里化函数:
```js
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存 _args 并收集所有的参数值
var _adder = function () {
_args.push(...arguments);
return _adder;
};
// 这个是最后输出的时候被调用的return 后面如果是函数体,
// 为了输出函数体字符串会自动调用 toString 方法
// 利用 toString 隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
// 这个 return 是第一次调用的时候返回上面的函数体,
// 这样后面所有的括号再执行的时候就是执行 _adder 函数体
return _adder;
}
console.log(add(1)(2)(3).toString()); // 6
console.log(add(1, 2, 3)(4).toString()); // 10
console.log(add(1)(2)(3)(4)(5).toString()); // 15
console.log(add(2, 6)(1).toString()); // 9
```
## 真题详解
- 什么是函数柯里化?
> 参考答案:
>
> 柯里化(*currying*)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
>
> 举个例子,就是把原本:
>
> *function(arg1,arg2)* 变成 *function(arg1)(arg2)*
> *function(arg1,arg2,arg3)* 变成 *function(arg1)(arg2)(arg3)*
> *function(arg1,arg2,arg3,arg4)* 变成 *function(arg1)(arg2)(arg3)(arg4)*
>
> 总而言之,就是将:
>
> *function(arg1,arg2,…,argn)* 变成 *function(arg1)(arg2)…(argn)*
-*EOF*-