JavaScript 函数柯里化:从入门到实战,一文搞定(面试可用)

前言

最近在复习 JavaScript 高阶函数的时候,又把函数柯里化(Currying)翻出来好好捋了一遍。很多人一听到"柯里化"就觉得高大上,其实它没那么神秘,用通俗的话说,就是把一个接受多个参数的函数,变成一个个只接受一个参数的函数链条。

这篇文章就把我自己的学习笔记整理了一下,从最基础的对比开始,慢慢讲到原理、实现、实际用法,希望能帮你把这个知识点彻底吃透。

1. 先看一个最直观的对比

普通写法:

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

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

柯里化写法:

JavaScript 复制代码
function add(a) {
  return function (b) {
    return a + b;
  };
}

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

看到区别了吗?

  • 普通版:一次把所有参数传完。
  • 柯里化版:参数一个一个传,每次调用返回一个新函数,直到所有参数收集齐了才真正计算。

这种"一个一个传"的方式,就是函数柯里化的核心。

2. 柯里化的本质:闭包 + 参数收集

为什么能一个一个传?靠的是闭包。

在外层函数里,参数 a 被保存了下来(成了闭包里的自由变量),内层函数可以随时访问它。当我们再传进来 b 的时候,就可以用之前保存的 a 去计算。

所以说,柯里化本质上就是利用闭包把参数"攒"起来,等参数够了再执行真正的逻辑。

3. 怎么判断参数"够了"?

JavaScript 函数有一个隐藏属性 length,它表示函数定义时参数的个数(不包括剩余参数和默认参数)。

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

console.log(add.length); // 2

我们可以利用这个属性来做一个相对严谨的柯里化判断:只有当收集到的参数数量 ≥ 原函数的 length 时,才真正执行。

4. 手写一个通用柯里化函数

下面这个是我自己最常用的一版:

JavaScript 复制代码
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args); // 直接展开,更清晰
    }
    return (...more) => curried(...args, ...more); // 这里也用展开合并
  };
}

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

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3));     // 6
console.log(curriedAdd(1, 2)(3));     // 6
console.log(curriedAdd(1)(2, 3));     // 6
console.log(curriedAdd(1, 2, 3));     // 6

这个版本的好处:

  • 支持任意数量的参数逐步传递
  • 也支持一次传多个(只要总数够了就执行)
  • 实现只有十来行,容易理解和记忆

注意:这里用了递归 + 闭包,外层每次调用都会产生新的 curried 函数,args 会被不断累加,直到满足条件。

5. 柯里化的经典实战场景

说了那么多,这么麻烦的编写柯里化函数,它到底能做什么呢?

场景一:固定部分参数,制造专用的工具函数

JavaScript 复制代码
// 通用日志函数
const log = (type) => (message) => {
  console.log(`[${type.toUpperCase()}]: ${message}`);
};

// 通过柯里化"固定"日志类型,得到专用函数
const errorLog = log('error'); // 第一个参数
const infoLog = log('info');
const warnLog = log('warn');
// 第二个参数
errorLog('接口 404 了!');         // [ERROR]: 接口 404 了!
infoLog('页面加载完成');           // [INFO]: 页面加载完成
warnLog('即将弃用旧 API');        // [WARN]: 即将弃用旧 API

这种写法在实际项目里特别常见,尤其是做日志、埋点、事件绑定的时候,能让代码语义更清晰。

场景二:延迟执行 / 参数复用

比如我们有一个通用的 Ajax 请求函数:

JavaScript 复制代码
function ajax(method, url, data) {
  // ...真正的请求逻辑
}

// 柯里化后
const get = curry(ajax)('GET');
const post = curry(ajax)('POST');

const fetchUserList = get('/api/users');
const fetchUserDetail = get('/api/users/');

const submitForm = post('/api/submit');

这样每次调用时就不用反复写 method,代码更简洁,也更不容易写错。

场景三:配合函数式编程库(如 lodash、ramda)

lodash 的 _.curry 功能更强大,支持占位符 __ 来跳过某些参数:

JavaScript 复制代码
const _ = require('lodash');

const join = (sep, ...arr) => arr.join(sep);
const curryJoin = _.curry(join);

const dotJoin = curryJoin('.');
dotJoin('a', 'b', 'c'); // "a.b.c"

不过日常项目里,自己手写一个简单版往往就够用了。

6. 柯里化的优缺点总结

优点:

  1. 参数复用:固定前几个参数,快速生成新函数
  2. 延迟执行:参数没收集齐之前不会真正运行
  3. 让代码更函数式、更声明式,阅读性更好(尤其配合管道操作)

缺点:

  1. 产生大量闭包和中间函数,性能略有损耗(现代引擎优化后影响很小)
  2. 调试时调用栈会变深一点
  3. 如果滥用,会让代码看起来"太巧妙",反而降低可读性

所以我的建议是:合适的地方用,别为了柯里化而柯里化。

最后

函数柯里化其实就是一个很小的技巧,但用好了能让你的代码更优雅、更灵活。尤其是当你开始接触函数式编程、React 高阶组件、Redux 中间件这些场景时,会发现柯里化的影子到处都是。

希望这篇从零开始的整理,能帮你把柯里化彻底搞明白。欢迎在评论区分享你用柯里化写过的有趣代码,或者你踩过的坑~

相关推荐
啃火龙果的兔子2 小时前
JavaScript 中的 Symbol 特性详解
开发语言·javascript·ecmascript
栀秋6662 小时前
你会先找行还是直接拍平?两种二分策略你Pick哪个?
前端·javascript·算法
漂流瓶jz2 小时前
PostCSS完全指南:功能/配置/插件/SourceMap/AST/插件开发/自定义语法
前端·javascript·css
南山安3 小时前
LangChain学习:Memory实战——让你的大模型记住你
前端·javascript·langchain
想用offer打牌3 小时前
如何开启第一次开源贡献之路?
java·后端·面试·开源·github
BD_Marathon4 小时前
Promise基础语法
开发语言·前端·javascript
千寻girling4 小时前
计算机组成原理-全通关源码-实验(通关版)---头歌平台
前端·面试·职场和发展·typescript·node.js
Aotman_4 小时前
JavaScript MutationObserver用法( 监听DOM变化 )
开发语言·前端·javascript·vue.js·前端框架·es6