好的,各位听众朋友们,早上好/下午好/晚上好! 今天咱们来聊聊 JavaScript 里两个听起来高大上,但其实挺接地气的概念:Currying(柯里化)和 Partial Application(偏应用)。 别被这俩名字唬住,它们说白了就是函数玩的花样,能让你的代码更灵活,更易读,更有…装逼范儿(开玩笑)。
开场白:函数界的变形金刚
想象一下,函数就像变形金刚,平时是个函数,关键时刻能变身成其他函数,或者只露一部分真身出来。柯里化和偏应用就是让函数变形的两种常用方式。
第一部分:Currying (柯里化) – “化整为零”的艺术
-
什么是柯里化?
简单来说,柯里化就是把一个接受多个参数的函数,变成一系列只接受一个参数的函数的过程。 每个函数都返回一个新的函数,直到所有参数都被传入为止,最后返回结果。
用大白话说: 原本你需要一次性喂给函数一大堆参数,柯里化后,你可以一个一个喂,每次喂完它都记住你喂的啥,直到喂饱为止。
-
柯里化的过程
假设我们有这样一个加法函数:
function add(x, y, z) { return x + y + z; }
柯里化后,它会变成这样:
function curriedAdd(x) { return function(y) { return function(z) { return x + y + z; } } } // 使用柯里化后的函数 const add5 = curriedAdd(5); // 返回一个等待 y 的函数 const add5and6 = add5(6); // 返回一个等待 z 的函数 const result = add5and6(7); // 返回最终结果:18 console.log(result); // 输出 18
或者更简洁一点:
const curriedAdd = x => y => z => x + y + z; // 使用柯里化后的函数 const add5 = curriedAdd(5); const add5and6 = add5(6); const result = add5and6(7); console.log(result); // 输出 18
-
柯里化的通用实现
上面的例子是针对特定函数的柯里化。 我们可以写一个通用的柯里化函数,让任何函数都能被柯里化:
function curry(fn) { const arity = fn.length; // 函数的参数个数 return function curried(...args) { if (args.length >= arity) { return fn.apply(this, args); } else { return function(...newArgs) { return curried.apply(this, args.concat(newArgs)); } } }; } // 使用通用的 curry 函数 const add = (x, y, z) => x + y + z; const curriedAdd = curry(add); const add5 = curriedAdd(5); const add5and6 = add5(6); const result = add5and6(7); console.log(result); // 输出 18 // 也可以一次性传入所有参数 const result2 = curriedAdd(1, 2, 3); console.log(result2); // 输出 6
代码解释:
curry(fn)
: 接受一个函数fn
作为参数,返回一个新的柯里化后的函数。arity = fn.length
: 获取函数fn
的参数个数,这很重要,因为我们需要知道什么时候参数已经全部传入。curried(...args)
: 返回的柯里化函数,使用剩余参数语法...args
收集传入的参数。if (args.length >= arity)
: 如果传入的参数个数大于等于函数fn
的参数个数,就说明所有参数都传入了,直接调用fn
并返回结果。 使用fn.apply(this, args)
可以确保函数在正确的上下文中执行,并将参数传递给它。else
: 如果传入的参数个数小于函数fn
的参数个数,就返回一个新的函数,这个新函数会继续收集参数,直到所有参数都传入为止。curried.apply(this, args.concat(newArgs))
: 将之前传入的参数args
和新传入的参数newArgs
合并,然后递归调用curried
函数。
-
柯里化的优势
- 延迟执行 (Lazy Evaluation): 参数可以一个一个传入,函数不会立即执行,直到所有参数都传入后才会执行。
- 代码复用: 可以创建一些预设了部分参数的新函数,方便在不同的场景中使用。
- 函数组合 (Function Composition): 柯里化是函数式编程的基础,方便进行函数组合。
-
柯里化的应用场景
- 事件处理: 可以预先设置一些事件处理函数的参数。
- 配置管理: 可以预先设置一些配置参数,然后在不同的模块中使用。
- 数据验证: 可以创建一些预设了验证规则的验证函数。
第二部分:Partial Application (偏应用) – “先喂一半”的策略
-
什么是偏应用?
偏应用是指创建一个新的函数,这个新函数预先绑定了原函数的部分参数。 当调用这个新函数时,只需要传入剩余的参数即可。
用大白话说: 原本你需要一次性喂给函数一大堆参数,偏应用允许你先喂一部分,得到一个已经记住这些参数的新函数,以后再喂剩下的。
-
偏应用的过程
继续使用上面的加法函数:
function add(x, y, z) { return x + y + z; }
偏应用后,我们可以创建一个预先绑定了
x
和y
的新函数:function partialAdd(x, y) { return function(z) { return add(x, y, z); } } // 使用偏应用后的函数 const add5and6 = partialAdd(5, 6); // 返回一个等待 z 的函数 const result = add5and6(7); // 返回最终结果:18 console.log(result); // 输出 18
-
偏应用的通用实现
function partial(fn, ...presetArgs) { return function(...laterArgs) { return fn.apply(this, presetArgs.concat(laterArgs)); } } // 使用通用的 partial 函数 const add = (x, y, z) => x + y + z; const add5and6 = partial(add, 5, 6); const result = add5and6(7); console.log(result); // 输出 18
代码解释:
partial(fn, ...presetArgs)
: 接受一个函数fn
和一些预设参数...presetArgs
作为参数,返回一个新的函数。(...laterArgs)
: 返回的新函数,使用剩余参数语法...laterArgs
收集之后传入的参数。fn.apply(this, presetArgs.concat(laterArgs))
: 调用原函数fn
,并将预设参数presetArgs
和之后传入的参数laterArgs
合并后传递给它。 使用fn.apply(this, ...)
可以确保函数在正确的上下文中执行。
-
偏应用的优势
- 代码复用: 可以创建一些预设了部分参数的新函数,方便在不同的场景中使用。
- 提高可读性: 可以将一些常用的参数预先绑定,使代码更易读懂。
- 简化函数调用: 可以减少函数调用时需要传入的参数个数。
-
偏应用的例子
假设我们有一个日志函数:
function log(level, message) { console.log(`[${level}] ${message}`); }
我们可以使用偏应用创建一个专门记录错误日志的函数:
const errorLog = partial(log, 'ERROR'); errorLog('Something went wrong!'); // 输出:[ERROR] Something went wrong!
第三部分:Currying vs Partial Application – 傻傻分不清?
很多人容易把柯里化和偏应用搞混,因为它们都涉及到预先设置函数的部分参数。 但它们之间还是有明显的区别的:
特性 | Currying (柯里化) | Partial Application (偏应用) |
---|---|---|
参数处理 | 每次只接受一个参数,返回一个接受下一个参数的函数。 | 可以一次接受多个参数,返回一个接受剩余参数的函数。 |
返回值类型 | 总是返回一个函数,直到所有参数都传入后才返回最终结果。 | 返回一个函数,这个函数接受剩余的参数并调用原函数。 |
参数应用顺序 | 必须按照参数的顺序依次传入。 | 可以一次性传入多个参数,不一定需要按照参数的顺序。 |
最终结果产生时间 | 只有在所有参数都传入后才会产生最终结果。 | 可以在部分参数传入后立即产生结果,也可以等待剩余参数传入后再产生结果。 |
总结:
- 柯里化 就像是把一个大饼切成一片一片的,每次吃一片,直到吃完整个饼。
- 偏应用 就像是先在饼上抹了一层酱,以后吃的时候只需要再加点料就行了。
举个更形象的例子:
假设你要做一道菜:糖醋排骨。
-
柯里化 就像是把做糖醋排骨的步骤分解成: 1. 买排骨, 2. 焯水, 3. 炸排骨, 4. 调糖醋汁, 5. 熬汁, 6. 倒入排骨。 你必须按照这个顺序一步一步来,每一步都产生一个新的“状态”,直到所有步骤都完成,才能做出糖醋排骨。
-
偏应用 就像是提前把糖醋汁调好(预设了糖、醋、酱油等参数),以后做糖醋排骨的时候,只需要把排骨炸好,然后倒入糖醋汁熬一下就可以了。 你提前完成了部分步骤,减少了后续的步骤。
第四部分:实战演练 – 让代码更优雅
-
数据验证
假设我们有一个验证函数,用于验证用户的年龄是否在指定范围内:
function isAgeValid(min, max, age) { return age >= min && age <= max; }
使用柯里化:
const curriedIsAgeValid = curry(isAgeValid); const isTeenager = curriedIsAgeValid(13)(19); // 创建一个验证是否为青少年的函数 console.log(isTeenager(15)); // 输出 true console.log(isTeenager(20)); // 输出 false
使用偏应用:
const isTeenager = partial(isAgeValid, 13, 19); // 创建一个验证是否为青少年的函数 console.log(isTeenager(15)); // 输出 true console.log(isTeenager(20)); // 输出 false
-
事件处理
假设我们有一个按钮,点击按钮后需要发送一个请求到服务器,并显示响应结果:
<button id="myButton">Click me!</button>
function fetchData(url, data, callback) { // 模拟发送请求 setTimeout(() => { const response = `Response from ${url} with data: ${JSON.stringify(data)}`; callback(response); }, 1000); } function showResult(result) { alert(result); } const myButton = document.getElementById('myButton'); // 使用偏应用预先设置 URL const fetchDataFromAPI = partial(fetchData, '/api/data'); myButton.addEventListener('click', () => { fetchDataFromAPI({ id: 123 }, showResult); });
在这个例子中,我们使用偏应用预先设置了
fetchData
函数的url
参数,这样在事件处理函数中只需要传入data
和callback
即可,使代码更简洁易懂。 -
日志记录
function log(level, logger, message) { logger(`[${level}] ${message}`); } const consoleLog = partial(log, 'INFO', console.log.bind(console)); const consoleWarn = partial(log, 'WARN', console.warn.bind(console)); const consoleError = partial(log, 'ERROR', console.error.bind(console)); consoleLog('Application started'); consoleWarn('Possible issue detected'); consoleError('Something went wrong');
第五部分:总结与展望
柯里化和偏应用是 JavaScript 中非常有用的函数式编程技巧。 它们可以提高代码的灵活性、可读性和可复用性。 虽然刚开始可能有点难以理解,但只要多加练习,就能掌握它们,并在实际开发中灵活运用。
记住,编程就像做菜,柯里化和偏应用就像是切菜和调料,用好了能让你的菜肴更美味(代码更优雅)。
今天的讲座就到这里,谢谢大家! 希望大家有所收获,并在实际开发中多多尝试使用柯里化和偏应用,让你的代码更上一层楼!