JS函数克里化

510 阅读4分钟

f1d87681f88ccda20fb97784d191dd52ae1b6253.jpeg

函数克里化是一种重要的编程技术,能将多参数函数转换为嵌套的单参数函数。面试和工作中,函数克里化常被问及。可以用于参数复用、函数组合以及延迟执行等场景。

函数克里化的基本概念

函数克里化的核心思想是将一个接收多个参数的函数转换成接收单一参数的函数,并返回一个新的函数,新函数继续接收下一个参数,以此类推,直到接收足够的参数执行原始函数的操作并返回结果。这种转换的过程可以利用闭包递归的特性来实现。

简单的数值相加

下面我们举个例子,实现两个数值的相加:

// 普通函数
function add(x, y) {
    return x + y
}

// 递归函数
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3,

无论是add()函数还是curryingAdd()函数,这都是特定的传参格式(必须按照函数传参)。

如果想实现 add(1, 2, 3) 三个数值的相加呢??

// 普通函数
function add(x, y, z) {
    return x + y + z
}

// 递归函数(注意)
function curryingAdd(x) {
    return function (y) {
        return function (z) {
           return x + y + z
        }
    }
}

add(1, 2, 3)           // 3
curryingAdd(1)(2)(3)   // 3,

不难看出,如果我们想实现支持任意数量参数的累加时,总不能一个个普通函数加下去或者一次次递归下去吧。

怎么解决呢?这时候就要用到克里化中将“多个参数的函数转换成只接受一个单一参数的函数”的思想了。

函数克里化实现

1、支持多参数累加

实现add(1)(2)(3)或者add(1)(2)(3)(4)

function add(x) {
    var currying = function (y) {
        x = x + y;
        return currying;
    };
    currying.toString = function () {
        return x;
    };
    return currying;
}
add(1)(2)(3)                // 6

分析:定义了一个 add 函数,它返回一个接受单一参数 x 的函数 currying。每次调用 currying 时,将新的参数与之前的累加结果相加,并返回 currying 自身,从而实现函数的逐步累加。通过 toString 方法,在最终调用时将累加结果隐式转换为字符串。

2、处理任意数量的参数

实现add(1, 2, 3)或者add(1, 2)(3)(4)

function add() {
    var args = Array.prototype.slice.call(arguments);
    var currying = function() {
        args.push(...arguments);
        return currying;
    };
    currying.toString = function () {
        return args.reduce(function (a, b) {
            return a + b;
        });
    }
    return currying;
}
add(1)(2)(3)            // 6
add(1, 2)(3)(4)             // 10
add(1)(2)(3)(4)(5)         // 15

分析:定义了一个 add 函数,使用闭包特性,通过 args 数组缓存传入的参数。每次调用 currying 函数时,将新的参数合并到 args 数组中,并返回 currying 自身,实现逐步累加的效果。最终,通过 toString 方法将累加结果转换为字符串,实现隐式转换。

var args = Array.prototype.slice.call(arguments); 的作用是将一个类数组对象 arguments 转换为真正的数组 args

让我们逐步解释这段代码:

  1. arguments:JavaScript 中的每个函数都有一个特殊的内部变量 arguments,它是一个类数组对象,包含了函数调用时传递的所有参数。即使函数定义时没有明确声明参数,也可以通过 arguments 对象来访问传递给函数的参数值。
  2. Array.prototype.sliceslice 是数组的一个方法,用于从原数组中截取一部分元素并返回一个新数组(args)。由于 arguments 是类数组对象,它并没有 slice 这个方法,但是我们可以借用 Array.prototype.slice 方法来操作它。
  3. Array.prototype.slice.call(arguments):这里通过借用 slice 方法,将 arguments 对象转换成一个真正的数组。call 方法用于调用一个函数,并将一个指定的对象设置为函数的上下文(也就是函数内部的 this)。由于 slice 不需要使用上下文,我们可以将 arguments 作为第一个参数传递给 call 方法,从而实现类数组对象向数组的转换。

如果不好理解,我们可以使用扩展运算符 ...

var args = [...arguments];

或者使用 Array.from() 方法:

var args = Array.from(arguments);

这些方法都能达到同样的效果:将 arguments 转换为一个真正的数组,以便更方便地处理函数的参数。

问题

这里直接使用console.log(add(1)(2)(3));,大家会发现输出的是一个函数,并不是最终的值。

image.png

这是由于 console.log() 在输出时会隐式地调用对象的 toString 方法,所以才会看到累加结果 6 被输出。但是在 add() 调用时,没有明确使用 console.log() 输出函数的返回值,所以没有直接看到累加结果。如果希望在调用 add() 时直接输出累加结果,可以这样做:

console.log(add(1)(2)(3).toString());

应用场景

函数克里化在实际开发中有许多应用场景,其中一些典型的例子包括:

  • 参数复用:通过固定部分参数,生成一个新的函数,方便在不同上下文中重复使用。
  • 函数组合:将多个只接受一个参数的函数组合起来,形成一个函数链,用于处理复杂的数据转换或计算。
  • 延迟执行:生成一个逐步求值的函数序列,最终在需要的时候一次性求值,提高性能和效率。