likes
comments
collection
share

理解函数柯里化

作者站长头像
站长
· 阅读数 3

理解函数柯里化

hello 大家好,今天小哆啦带大家学习函数柯里化。

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

高阶函数

1. 接受函数作为参数

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

function operateOnArray(array, operation) {
    return array.map(operation);
}

const numbers = [1, 2, 3, 4, 5];
const squared = operateOnArray(numbers, function (num) {
    return num * num;
});

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

2. 返回函数(闭包)

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

function multiplyBy(factor) {
    return function (number) {
        return number * factor;
    };
}

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

3. 函数组合

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

function compose(f, g) {
    return function (x) {
        return 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

4. 回调函数:

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

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

fetchData(function (result) {
    console.log(result); // 处理异步获取的数据
});

5. 函数柯里化

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

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. fn.length 这是 JavaScript 中的一个内建属性,它返回一个函数所声明的形参的数量。例如,对于 function add(a, b, c)add.length 的值是 3,因为该函数有三个形参。

  2. args.length 这是一个数组(args)的属性,它表示某个时刻已经传递给柯里化函数的参数数量。例如,在调用 curriedAdd(1)(2)(3) 时,args.length 的值在不同的步骤分别是 123

  3. args 这是一个数组,包含了之前调用柯里化函数时传入的所有参数。每次调用新的柯里化函数时,都会将当前传入的参数追加到 args 数组中。

  4. moreArgs 这是新调用柯里化函数时传入的参数。每次调用新的柯里化函数时,将新传入的参数追加到 moreArgs

在这个上下文中,fn.lengthargs.length 是用来检查函数所需参数数量的。在函数柯里化的实现中,我们通过比较 args.lengthfn.length 来判断是否已经传递了足够的参数,以便执行原始函数。

这个挺难理解,下面是一步一步的解释 当调用 curriedAdd(1)(2)(3) 时,整个执行过程可以分为以下几个步骤:

  1. 调用 curriedAdd(1)
    • args 变成 [1]
    • 返回一个新函数 (...args) => curried(...args, ...moreArgs)
  2. 调用返回的新函数 (...args) => curried(...args, ...moreArgs),即 curriedAdd(1)(2)
    • args 变成 [1, 2]
    • 返回另一个新函数 (...args) => curried(...args, ...moreArgs)
  3. 再次调用返回的新函数 (...args) => curried(...args, ...moreArgs),即 curriedAdd(1)(2)(3)
    • args 变成 [1, 2, 3]
    • 此时参数数量达到了原始函数 add 的参数数量,执行 curried(...args)
  4. 执行 curried(1, 2, 3)add(1, 2, 3)
    • 返回结果 1 + 2 + 3,即 6

其实总结来说就是:判断传入的参数是否达到add函数所需要的参数,如果没有达到就继续存储参数直到达到之后,进行函数调用

说完高阶函数咱们再说说函数柯里化

函数柯里化

什么是柯里化

先给出函数柯里化的定义:在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

有没有一种感觉读完和没读一样哈哈

咱们来看一个案例吧,不看案例还真理解不了

function sum (a, b, c) {
    console.log(a + b + c);
}
sum(1, 2, 3); // 6

这个是一个累加函数,接收三个参数,但是假设我有这样一种需求就是前两个参数不变,然后最后一个参数可以随意

很多人,第一想法就是传入前两个不改不就行了。其实这个也能说是一个解决方案,但是代码得优雅,经常一调用就得穿前面那俩个固定的参数是不是很繁琐而且容易出错,出错之后还不好排查,对吧。

再去分析这个需求,咱们先不管函数的具体实现,这个需求的调用写法sum(1, 2)(3);这样 sum(1, 2)(10); 。就是,先把前2个参数的运算结果拿到后,再与第3个参数相加。这个就是函数柯里化的简单应用

我明明可以一步写完就像第一段代码那样,我干嘛还要柯里化那样麻烦。 接下来就为大家解疑:

  • 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理;
  • 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果。让函数的职责单一。
  • 并且经过柯里化的函数有的地方可以直接进行逻辑复用

柯里化实现

sum(1, 2)(3); 这样的写法,并不常见。拆开来看,sum(1, 2) 返回的应该还是个函数,因为后面还有 (3) 需要执行。其实你会发现sum(1,2)其实是不是返回的就是一个函数,然后函数在调用传参才返回的最终结果。 然后反过来,从最后一个参数,从右往左看,它的左侧必然是一个函数。以此类推,如果前面有n个(),那就是有n个函数返回了结果,只是返回的结果,还是一个函数。是不是有点递归的意思?

来咱们一起看看代码

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

其实上面在高阶函数的时候我已经详细说过柯里化执行的顺序,接下来咱们对应着这个代码在对应咱们刚刚的分析详细说说

 return function (...moreArgs) {
                return curried(...args, ...moreArgs);
            };

咱们看这个地方是不是在参数不满足的时候重新回调curried函数呀,这个是不是符合咱们分析的第二点,有点递归的感觉

if (args.length >= fn.length) {
	return fn(...args);
}

然后咱们再看他递归跳出的条件args.length >= fn.length这个地方是不是参数已经够了对吧然后调用原函数fn这样简单的函数柯里化就实现了

柯里化优点

1、单一职责的原则

现在我们修改约束条件为:将第一个参数加上2,第二个参数乘以2,第三个参数平方 如下示例代码展示了,函数柯里化的单一职责原则:

// 方式一:正常情况下我们会将简单的处理逻辑这样写(代码逻辑很少)
// 但是如果num1的处理逻辑有20行 ,num2的处理逻辑有20行 ,num3的处理逻辑有20行 ,这样处理起来函数会变得很复杂,之后要修改也不容易去操作
function sum1 (num1, num2, num3) {
    num1 = num1 + 2
    num2 = num2 * 2
    num3 = num3 * num3
    return num1 + num2 + num3
}
var result1 = sum1(1, 2, 3)
console.log(result1) // 16

// 方式二:
// 函数柯里化单一职责原则,每一个函数中都有一个对应的职责,修改起来也很方便
function sum2 (num1) {
    num1 = num1 + 2
    return function (num2) {
        num2 = num2 * 2
        return function (num3) {
            num3 = num3 * num3
            return num1 + num2 + num3
        }
    }
}
var result2 = sum2(1)(2)(3)
console.log(result2) // 16

两种方式的输出结果一致,但第二种使用了柯里化实现了单一职责原则

2、逻辑的复用

案例一: 假设在程序中,需要经常把5和一个数值相加的出结果,实现方法示例代码如下:

// 方式一:
function sum (x, y) {
    return x + y
}
console.log(sum(5, 1))
console.log(sum(5, 2))

// 方式二:函数柯里化
function add (num1) {
    return function (num2) {
        return num1 + num2
    }
}
// 使用方法一:
var result1 = add(5)(10)
console.log(result1) // 15
// 使用方法二:直接将add(5)存储在一个变量中,使用的时候只需要传入要与5相加的数字
var add5 = add(5)
var result2 = add5(10)
console.log(result2) // 15

适合使用的场景

函数柯里化的适用场景有:

  1. 参数复用: 当一个函数有多个参数,但在不同的上下文中,部分参数是相同的,柯里化可以方便地复用这些参数。

    // 非柯里化版本
    function add(a, b, c) {
      return a + b + c;
    }
    
    const result1 = add(1, 2, 3); // 6
    const result2 = add(1, 2, 5); // 8
    
    // 柯里化版本
    function curryAdd(a) {
      return function (b) {
        return function (c) {
          return a + b + c;
        };
      };
    }
    
    const curryResult1 = curryAdd(1)(2)(3); // 6
    const curryResult2 = curryAdd(1)(2)(5); // 8
    
  2. 延迟执行: 柯里化可以延迟函数的执行,逐步传递参数,使得函数可以在需要时执行。

    function curryAdd(a) {
      return function (b) {
        return function (c) {
          return a + b + c;
        };
      };
    }
    
    const addToTwo = curryAdd(2);
    const result = addToTwo(3)(5); // 先传递 3,再传递 5,最后执行,结果为 10
    
  3. 函数组合: 柯里化允许函数逐步组合,形成更复杂的功能。

    function add(a, b, c) {
      return a + b + c;
    }
    
    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) {
          return fn(...args);
        } else {
          return function (...moreArgs) {
            return curried(...args, ...moreArgs);
          };
        }
      };
    }
    
    const curriedAdd = curry(add);
    
    const add5 = curriedAdd(5);
    const add5And6 = add5(6);
    const finalResult = add5And6(7); // 5 + 6 + 7 = 18
    

函数柯里化的核心在于:函数里面返回函数,从而做到参数复用的目的。

此外

函数柯里化在某种程度上与装饰器模式有一些相似之处。

  1. 参数的逐步传递:
    • 在函数柯里化中,函数的参数被逐步传递,每一步都返回一个新的函数,允许更灵活地处理参数。
    • 在装饰器模式中,装饰器也可以逐步应用于对象,每一层都可以在不改变原始对象结构的情况下添加新的行为。
  2. 链式调用:
    • 函数柯里化允许通过链式调用一系列的函数,逐步构建完整的功能。
    • 装饰器模式也可以通过链式调用多个装饰器,逐步增强对象的功能。
  3. 动态组合:
    • 函数柯里化允许动态地组合函数,通过部分应用参数来创建新的函数。
    • 装饰器模式允许动态地组合装饰器,通过将它们应用于对象来创建新的对象。

虽然存在这些相似之处,但也要注意它们的不同:

  • 函数柯里化更注重于将函数的参数进行逐步分解和组合,强调函数的纯粹性和不可变性。
  • 装饰器模式更注重于在不改变原始对象的结构的情况下,动态地增强对象的功能。

总结

函数的柯里化,是 JavaScript 中函数式编程的一个重要概念。它返回的,是一个函数的函数。其实现方式,需要依赖参数以及递归,通过拆分参数的方式,来调用一个多参数的函数方法,以达到减少代码冗余,增加可读性的目的。