柯里化(Currying)是一种函数式编程的技术,其主要思想是将一个多参数的函数转换成一系列单参数的函数。这使得我们可以逐步传递参数,每次传递一个参数,返回一个新的函数,直到所有参数都被收集完毕并执行原始函数。这种技术的名字来源于数学家 Haskell Curry。

柯里化不会调用函数。它只是对函数进行转换。

高阶函数

柯里化函数是高阶函数的一种特殊应用。高阶函数是指能够接受一个或多个函数作为参数,并且/或者返回一个新函数的函数。在JavaScript中,函数是第一类对象,这使得高阶函数成为一种常见的编程模式。

接受函数作为参数

高阶函数可以接受一个或多个函数作为参数。这样的函数通常用于执行传入的函数,或者基于传入的函数执行一些逻辑。

1
2
3
4
5
6
7
8
function operateOnArray(array, operation) {
return array.map(operation);
}

const numbers = [1, 2, 3, 4, 5];
const squared = operateOnArray(numbers, num => num ** 2);

console.log(squared); // [1, 4, 9, 16, 25]

返回函数(闭包)

高阶函数可以返回一个新的函数。 这样的函数通常用于封装、延迟执行或者组合其他函数。

1
2
3
4
5
6
function multiplyBy(factor) {
return number => number * factor;
}

const multiplyBy2 = multiplyBy(2);
console.log(multiplyBy2(5)); // 10

函数组合

高阶函数可以将多个函数组合在一起形成新的函数。 这样的函数组合可以提高代码的可读性和复用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function compose(f, g) {
return x => f(g(x))
}

function double(x) {
return x * 2;
}

function square(x) {
return x * x;
}

const doubleThenSquare = compose(square, double);
console.log(doubleThenSquare(3)); // 先翻倍再平方:36

回调函数

高阶函数常常用于处理异步操作,通过回调函数来执行一些逻辑。

1
2
3
4
5
6
7
8
9
function fetchData(callback) {
// 模拟异步操作
setTimeout(function () {
const data = [1, 2, 3, 4, 5];
callback(data);
}, 1000);
}

fetchData(result => console.log(result)); // 处理异步获取的数据);

函数柯里化

柯里化是一种将接受多个参数的函数转化为一系列接受一个参数的函数的技术。这可以通过高阶函数来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return function (...moreArgs) {
return curried(...args, ...moreArgs);
};
}
};
}

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

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6

柯里化的优点

参数复用

柯里化允许我们部分应用函数并在之后的调用中重复使用这部分应用的函数。这对于在不同上下文中多次使用相同的参数集合非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
// 普通加法函数
function add(a, b, c) {
return a + b + c;
}

// 使用柯里化创建新的加法函数
const curryAdd = curry(add);

// 部分应用并重复使用
const add2 = curryAdd(2);
console.log(add2(3)(4)); // 输出:9
console.log(add2(1)(5)); // 输出:8

在上面的例子中,我们部分应用了加法函数,并创建了一个新的函数add2,它在之后的调用中一直使用参数2

延迟执行

柯里化使得我们可以逐步传递参数,延迟函数的执行。这对于需要等待所有参数就绪的场景非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
// 普通乘法函数
function multiply(a, b, c) {
return a * b * c;
}

// 使用柯里化创建新的乘法函数
const curryMultiply = curry(multiply);

// 逐步传递参数并延迟执行
const multiplyByTwo = curryMultiply(2);
const multiplyByTwoAndThree = multiplyByTwo(3);
console.log(multiplyByTwoAndThree(4)); // 输出:24

在上面的例子中,我们逐步传递参数,最终在第三步中执行乘法函数。

函数组合

柯里化方便了函数的组合,可以将多个单参数函数组合成一个函数链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 普通函数
function square(x) {
return x * x;
}

function double(x) {
return x * 2;
}

// 使用柯里化创建新的函数链
const currySquare = curry(square);
const curryDouble = curry(double);

// 组合函数链
const squareAndDouble = curryDouble(currySquare(3));

console.log(squareAndDouble()); // 输出:18,先平方后翻倍

在上面的例子中,我们使用柯里化创建了两个单参数函数,并将它们组合成了一个函数链。

柯里化的缺点

  • 可能会降低性能(但跟操作DOM相比可以忽略不计) 。通过柯里化,函数的性能可能会降低,因为需要额外的内存来存储函数的返回值和参数。
  • 可能会增加代码复杂度。通过柯里化,可能会增加代码的复杂度,因为需要处理额外的参数和函数返回值。

柯里化的实现

普通实现

普通实现就是把一个接受多参数的函数变成每次直接收一个参数的函数,其中用到了闭包和高阶函数的知识。

也可以使用lodash 库的 _.curry函数实现柯里化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function curry(f) { // curry(f) 执行柯里化转换
return function(a) {
return function(b) {
return f(a, b);
};
};
}

// 用法
function sum(a, b) {
return a + b;
}

let curriedSum = curry(sum);

console.log( curriedSum(1)(2) ); // 3

正如上面看到的,实现非常简单:只有两个包装器(wrapper)。

  • curry(func) 的结果就是一个包装器 function(a)
  • 当它被像 curriedSum(1) 这样调用时,它的参数会被保存在词法环境中,然后返回一个新的包装器 function(b)

然后这个包装器被以 2 为参数调用,并且,它将该调用传递给原始的 sum 函数。

柯里化更高级的实现,例如 lodash 库的 _.curry,会返回一个包装器,该包装器允许函数被正常调用或者以偏函数(partial)的方式调用:

1
2
3
4
5
6
7
8
function sum(a, b) {
return a + b;
}

let curriedSum = _.curry(sum); // 使用来自 lodash 库的 _.curry

alert( curriedSum(1, 2) ); // 3,仍可正常调用
alert( curriedSum(1)(2) ); // 3,以偏函数的方式调用

通用的函数柯里化

实现原理:

  • 将一个函数fn传入进行柯里化,返回一个curried的函数(已经柯里化好了)

  • curried传参:

    • 如果传的参数个数$\ge$原函数fn的参数个数即fn.length (arity参数数量):返回fn(参数) 的执行结果。
    • 否则,如果传的参数个数$\lt$原函数fn的参数个数即fn.length ,递归地进行柯里化curried ,接受新的参数与之前的参数合并(可以使用剩余参数),直到参数个数达到了原函数fn的参数个数即fn.length ,到达递归出口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 柯里化函数
function curry(fn){
// 获得函数fn的形参个数
const arity = fn.length
return function curried(...args){
if(args.length >= arity){
return fn.apply(this, args)
}else {
return (...rest)=>{
return curried.apply(this, args.concat(rest))
}
}
}
}

使用看看:

1
2
3
4
5
6
const sum = (a, b, c)=> a+b+c
const add = curry(sum)
console.log(add(1, 2, 3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1)(2, 3)); // 6
console.log(add(1)(2)(3)); // 6