前端大白话之"库里"化(Currying)
前期知识点:闭包,arguments,伪数组与数组的转换,合并数组。
啥是Currying?
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
Currying 的重要意义在于可以把函数完全变成接受一个参数,返回一个值的固定形式,函数curry之后,可以只提供一个参数,其他的参数作为这个函数的“环境”来创建。这就能让函数回归到原始的一个参数进去一个值出来的状态,它既能减少代码冗余,也能增加可读性,这样对于讨论和优化也会更加方便。
Currying(柯里化)是如何工作的?
它的工作方式是通过为每个可能的参数嵌套函数,使用由嵌套函数创建的自然闭包来保留对每个连续参数的访问。
大白话
日常看完概念不懂啥意思?
中文中XX化就是一个事物的变化,比如美化、丑化、情绪化、复杂化。
柯里化简单来说首先它是一个对函数(方法)的包装。
包装成什么样子呢?
把多个参数的原函数包装成可以接受一个参数的样子。举个栗子:
function addition(a, b) {
return a + b;
}
addition(1, 2); // 3
// 让其支持
addition(1)(2); // 3
观察addition(1)(2)
,凭什么这个函数可以连续调用呢?说明addition(1)
的返回值还是一个函数。
所以就有了这样的实现,话不多说,先上代码。
function currying(fn, ...args) {
// 如果参数个数小于最初的 fn.length,则递归调用,继续收集参数
// 这里 fn.length是指声明函数的参数的个数
if (args.length < fn.length) {
return (...newArgs) => currying(fn, ...args, ...newArgs);
} else {
return fn(...args);
}
}
function addition(a, b, c) {
return a + b + c;
}
let sum = currying(addition);
sum(3);// 原函数 addition中声明了a b c三个参数,这里我们只传了一个参数3,所以args.length < fn.length,返回值还是一个函数
sum(1,2)(3,4);// 6 这里我们先传了两个参数1 2,所以sum(1,2)返回值依然是一个函数。紧接着再次调用传入3 4,一共传了4个参数。所以args.length > fn.length,所以返回fn(...args)也就是addition(1,2,3,4)
进阶
机智的我很快就不能满足于此,因为我发现这样不能一直调用,因为当传入参数的个数比原函数声明的参数多的时候,返回的不再是一个函数,所以不能连续一直调用。先来看看我写的这个函数:
function addition() {
if (!arguments.length) return;
return [...arguments].reduce((a, b) => a + b);
}
这个函数声明的参数甚至是0
。我想写一个currying
函数让其可以currying(addition)(1)(2)(3)(4,5)(6,7,8)(9)
随意调用,这样多爽!
先看一道面试题:
//实现一个add方法,使计算结果能够满足如下预期:
// add(1)(2)(3) = 6;
// add(1, 2, 3)(4) = 10;
// add(1)(2)(3)(4)(5) = 15;
直接上解法
function add(...args) {
// 第一次执行时,定义一个数组专门用来存储所有的参数
let _args = args || [];
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
let _adder = function (...newArgs) {
_args = _args.concat(newArgs); //_args = [..._args,...newArgs]
return _adder;
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce((a, b) => a + b);
};
return _adder;
}
// console.log(add(1)(2, 4)); // f 7
// console.log(add(1)(2)(3)); // f 6
// console.log(add(1, 2, 3)(4)); // f 10
// console.log(add(1)(2)(3)(4)(5)); // f 15
// console.log(add(2, 6)(1)); // f 9
// console.log(typeof add(1)(2)); // function
// console.log(add(1)(2).toString()); // 3 <number>
// console.log(String(add(1)(2))); // 3 <string>
// console.log(Number(add(1)(2))); // 3 <number>
// console.log(add(1)(2) - 0); // 3 <number>
由此引申出一个通用的currying
function currying(fn, ...args1) {
// 若未传入fn则return
if (!fn) return;
const that = this;
// 定义一个数组存储每次传入的参数
let args = args1 || [];
// 定义一个函数 foo,并最后返回这个函数 foo,使 currying 后的函数可以继续调用。
let foo = function (...args2) {
// 通过 currying 后函数传入的参数来判断是让其继续返回函数还是返回结果(值)
if (args2.length === 0) {
// 若currying 后的函数未传参,比如 currying(addition)()或者currying(addition)(1)(),则返回结果
return fn.call(that, ...args);
} else {
// 否则继续返回此函数让其继续收集参数
args = [...args, ...args2];
return foo;
}
};
// 最后返回定义的函数 foo
return foo;
}
function addition() {
if (!arguments.length) return;
return [...arguments].reduce((a, b) => a + b);
}
function subtraction(a, b) {
return a - b;
}
function multiplication(a, b) {
return a * b;
}
function division(a, b) {
return a / b;
}
console.log(currying(addition)(1)()); // 1
console.log(currying(addition)(2)(3)()); // 5
console.log(currying(multiplication, 5)(4)()); // 20
柯里化的作用
- 参数复用
- 提前返回,提高适用性
- 延迟执行——函数的主体本身不执行,可以看成是延迟执行
Function.prototype.bind 方法也是柯里化应用。