别再背柯里化面试题了,看完这篇你自己也会写

深入理解JavaScript柯里化:从入门到实践

前言

最近在学习面试题时,经常看到"柯里化"这个词。说实话,第一次看到 add(1)(2) 这种写法时,我也是一脸懵:为什么函数要这样调用?这样写有什么好处?

通过一段时间的学习,我终于搞懂了柯里化的原理和应用。这篇文章会按照我的学习路径,用最简单的方式,带你一步步理解柯里化。

第一章:从一个最简单的函数开始

1.1 我们最熟悉的函数调用

先来看一个最基础的加法函数:

javascript 复制代码
// 1.js
function add(a, b) {
    return a + b
}

console.log(add(1, 2)) // 3

这是我们最熟悉的函数调用方式------一次性传入所有参数。这种方式简单直观,但有没有想过:如果我想先传入第一个参数,过一会儿再传入第二个参数,该怎么办?

1.2 尝试分步传入参数

JavaScript给了我们一种可能性------函数可以作为返回值

javascript 复制代码
// 2.js
function add(a) {
    return function(b) {
        return a + b
    }
}

console.log(add(1)(2)) // 3
  • 打印结果

看到区别了吗?原来的 add(1, 2) 变成了 add(1)(2)

这就是手动柯里化的雏形------将一个接收多个参数的函数,转换成一系列接收单个参数的函数。

1.3 理解这个过程的本质

让我们拆解一下 add(1)(2) 的执行过程:

javascript 复制代码
// 第一步:调用 add(1)
const fn = add(1)  // fn 是一个函数:function(b) { return 1 + b }

// 第二步:调用 fn(2)
const result = fn(2)  // 3

// 合并起来就是 add(1)(2)
  • 打印结果

关键点 :这里利用了闭包 的特性------内部函数可以访问外部函数的变量 a。即使外部函数执行完毕,变量 a 也不会被销毁,因为它被内部函数引用了。

第二章:手动柯里化的局限性

2.1 如果函数需要三个参数呢?

javascript 复制代码
// 两个参数的版本
function add2(a) {
    return function(b) {
        return a + b
    }
}

// 三个参数的版本
function add3(a) {
    return function(b) {
        return function(c) {
            return a + b + c
        }
    }
}

console.log(add3(1)(2)(3)) // 6

发现问题了吗?每增加一个参数,就要多嵌套一层函数

2.2 如果有多个函数需要柯里化

javascript 复制代码
function add(a, b, c) { return a + b + c }
function multiply(a, b, c) { return a * b * c }
function concat(a, b, c) { return '' + a + b + c }

// 每个函数都要手动改写成嵌套函数,太麻烦了!
function addCurried(a) {
    return function(b) {
        return function(c) {
            return a + b + c
        }
    }
}
// ... 还要为 multiply、concat 每个函数都写一遍,这样大麻烦了,所以我们来设计一种新的模式

第三章:通用柯里化函数的实现

3.1 设计思路

我们需要一个"柯里化工厂",它能:

  1. 接收一个普通函数作为参数
  2. 返回一个可以收集参数的函数
  3. 当参数收集够了,才执行原函数

3.2 第一步:获取函数需要的参数个数

javascript 复制代码
// 3.js
function add(a, b) {
    return a + b
}

// 函数的 length 属性表示它期望的参数个数
console.log(add.length) // 2

每个函数都有一个 length 属性,返回函数定义的参数个数。这正好可以用来判断参数是否收集够了。

  • 打印结果

3.3 第二步:实现柯里化函数

javascript 复制代码
function add(a, b) {
    return a + b
}

function curry(fn) {
    // 返回一个函数,用于收集参数
    return function curried(...args) {
        // 如果收集的参数数量 >= 原函数需要的参数数量
        if (args.length >= fn.length) {
            // 参数够了,直接执行原函数
            return fn(...args)
        }
        // 参数还不够,返回一个新函数继续收集
        return (...rest) => curried(...args, ...rest)
    }
}

const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)) // 3
  • 打印结果

3.4 理解执行过程

让我们一步步追踪 curriedAdd(1)(2) 的执行:

javascript 复制代码
// 第一次调用 curriedAdd(1)
// args = [1]
// args.length(1) < fn.length(2),返回新函数
// 返回 (...rest) => curried([1], ...rest)

// 第二次调用 (2)
// rest = [2]
// 调用 curried([1], [2]) => curried(1, 2)
// 此时 args = [1, 2]
// args.length(2) >= fn.length(2)
// 执行 add(1, 2) 返回 3

核心机制

  • 闭包:每次调用都通过闭包保存已收集的参数
  • 递归:参数不够就返回新函数,继续收集
  • 退出条件:参数数量达到原函数的要求

第四章:为什么要用 ...rest?

4.1 一个常见的疑问

有同学可能会问:既然柯里化是一个参数一个参数地传,为什么实现时要写成 (...rest) => curried(...args, ...rest),而不是 (arg) => curried(...args, arg)

4.2 两种实现的对比

javascript 复制代码
// 版本A:使用 ...rest(我们的实现)
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args)
        }
        return (...rest) => curried(...args, ...rest)  // 可以接收多个参数
    }
}

// 版本B:只接收单个参数
function curry2(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args)
        }
        return (arg) => curried(...args, arg)  // 只能接收一个参数
    }
}

const curriedAdd = curry(add)
const curriedAdd2 = curry2(add)

// 测试不同的调用方式
console.log(curriedAdd(1)(2))        // ✅ 3
console.log(curriedAdd(1, 2))        // ✅ 3(版本A支持)
console.log(curriedAdd2(1)(2))       // ✅ 3
console.log(curriedAdd2(1, 2))       // ❌ 第二个参数丢失(版本B不支持)

4.3 为什么需要支持多个参数?

考虑一个更复杂的函数:

javascript 复制代码
function greet(greeting, name, punctuation) {
    return `${greeting}, ${name}${punctuation}`
}

const curriedGreet = curry(greet)

// 实际开发中,我们可能需要各种调用方式
curriedGreet('Hello')('World')('!')           // 分三次传
curriedGreet('Hello', 'World')('!')           // 前两个一起传
curriedGreet('Hello')('World', '!')           // 后两个一起传
curriedGreet('Hello', 'World', '!')           // 一次性传完

// 使用 ...rest 的实现支持以上所有方式
// 使用固定参数的实现只支持第一种方式

结论...rest 的存在让柯里化函数更灵活,能够适应各种调用场景。

第五章:柯里化的实际应用

理论说完了,来看看柯里化在实际开发中能做什么。

5.1 固定函数参数(最常用)

javascript 复制代码
// 4.js
// 日志函数
const log = type => message => {
    console.log(`${type}: ${message}`)
}

// 固定日志类型,创建专用的日志函数
const errorLog = log('error')
const infoLog = log('info')
const warnLog = log('warn')

// 使用时只需关注消息内容
errorLog('数据库连接失败') // error: 数据库连接失败
infoLog('用户登录成功')    // info: 用户登录成功
warnLog('磁盘空间不足')    // warn: 磁盘空间不足
  • 打印结果

这样做的好处:

  • 提升语义errorLoglog('error') 更容易理解
  • 减少重复:不用每次都传 'error'
  • 便于维护 :如果日志格式要改,只需修改 log 函数

5.2 延迟计算(大家可以复制代码自己运行输出一下)

javascript 复制代码
const add = curry((a, b, c) => a + b + c)

// 这些调用都没有真正计算
const add1 = add(1)        // 还在收集参数
const add1And2 = add1(2)   // 还在收集参数

// 直到参数齐了才计算
const result = add1And2(3) // 6,真正执行加法

5.3 参数复用(大家可以复制代码自己运行输出一下)

javascript 复制代码
// 处理数组的函数
const map = curry((fn, arr) => arr.map(fn))
const filter = curry((fn, arr) => arr.filter(fn))

// 复用转换逻辑
const double = map(x => x * 2)          // 创建一个"加倍"函数
const even = filter(x => x % 2 === 0)   // 创建一个"取偶数"函数

const numbers = [1, 2, 3, 4, 5]

// 应用到不同的数组
console.log(double(numbers)) // [2, 4, 6, 8, 10]
console.log(even(numbers))   // [2, 4]

const moreNumbers = [10, 20, 30]
console.log(double(moreNumbers)) // [20, 40, 60] 复用 double 逻辑

5.4 与函数组合配合(大家可以复制代码自己运行输出一下)

javascript 复制代码
// 函数组合:从右往左执行
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x)

// 创建一些柯里化函数
const addOne = curry((x, y) => x + y)(1)
const double = x => x * 2
const square = x => x * x

// 组合成新函数:先加1,再乘2,再平方
const addOneThenDoubleThenSquare = compose(square, double, addOne)

console.log(addOneThenDoubleThenSquare(3)) 
// 3 -> 加1得4 -> 乘2得8 -> 平方得64

第六章:总结与思考

6.1 柯里化的核心要点

通过本文的学习,我们掌握了:

  1. 什么是柯里化:将多参数函数转换成一系列单参数函数的嵌套调用
  2. 柯里化的本质:利用闭包收集参数,当参数数量满足条件时执行函数
  3. 柯里化的实现:闭包 + 递归,配合 fn.length 判断参数是否足够
  4. 灵活性的设计:使用 ...rest 支持多种参数传递方式
  5. 实际应用场景:固定参数、延迟计算、参数复用、函数组合等

6.2 柯里化 vs 普通函数调用

对比维度 普通函数 柯里化函数
调用方式 fn(a, b, c) fn(a)(b)(c)fn(a, b)(c)
参数传递 一次性 可分步
适用场景 简单调用 参数复用、延迟计算
学习成本

最后

柯里化不仅仅是一个面试题,更是一种实用的编程思想。它让我们的代码更加灵活、可复用,也是函数式编程中的重要概念。

希望这篇文章能帮助正在学习柯里化的你。如果有什么理解不到位的地方,欢迎在评论区交流讨论!


相关推荐
snowfoootball1 小时前
优先队列/堆 题目讲解
学习·算法
SamtecChina20231 小时前
Samtec连接器设计研究 | 载流量:温升为什么重要?
大数据·网络·人工智能·算法·计算机外设
程序员南飞1 小时前
排序算法举例
java·开发语言·数据结构·python·算法·排序算法
adore.9681 小时前
2.24 oj95 96 97
开发语言·c++·算法
白中白121382 小时前
算法题-16
算法
梦帮科技2 小时前
【DREAMVFIA开源】量子互联网协议:节点通信与路由算法
人工智能·神经网络·算法·生成对抗网络·开源·量子计算
菜鸡儿齐2 小时前
leetcode-搜索插入位置
数据结构·算法·leetcode
52Hz1182 小时前
力扣394.字符串解码、739.每日温度、84.柱状图中最大的矩形
python·算法·leetcode
Yzzz-F2 小时前
牛客寒假算法训练营4
算法