深入理解 JavaScript 中的柯里化

前言

本文将详细介绍JS中的一个关于函数的重难点--柯里化。

什么是柯里化

柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术,名称来源于数学家 Haskell Curry。在数学领域,柯里化用于将多元函数转换为一元函数的链式调用,这种转换保持了函数的可组合性和数学性质。

在编程中,柯里化允许我们分步传递参数,当所有参数都传递完毕后,函数才会真正执行。这种技术使得函数更加灵活,便于复用和组合。

柯里化的实现过程

在该部分,我们先引入rest语法,为后面的讲解进行铺垫,然后介绍柯里化的具体实现过程,以及详细分析该过程中的递归调用。

rest 语法... 展开数组

在 ES6 中,rest 参数语法(...)允许我们将不定数量的参数表示为一个数组。这对于柯里化实现非常重要:

展开参数:

js 复制代码
function sum(...numbers) {
    return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

展开数组:

js 复制代码
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

如何柯里化--递归

下面是一个基本的柯里化函数实现:

让我们从一个简单的加法函数开始:

js 复制代码
function add(a, b, c) {
    return a + b + c;
}
console.log(add(1, 2, 3)); // 6

柯里化:

js 复制代码
function curry(fn) {
    // fn 是被柯里化的原始函数 例如我们待会要传add
    // 通过闭包保持对fn的引用
    
    let judge = (...args) => {
        // 使用ES6 rest参数收集所有传入参数
        // 这里由于闭包可以访问到外层函数的fn变量
        
        if (args.length === fn.length) {
            // 终止条件:当收集的参数数量等于原函数参数数量时
            return fn(...args); // 展开参数执行原函数
        }
        
        // 递归逻辑
        return (...newArgs) => judge(...args, ...newArgs);
    }
    
    return judge; // 返回柯里化后的函数 例如我们待会要将judge给addCurry
}

let addCurry = curry(add);  //进行柯里化 addCurry就是函数返回的judge

addCurry(1)(2)(3);  //柯里化调用 递归

柯里化执行过程分析

1. 初始化阶段

js 复制代码
let addCurry = curry(add);
  • curry函数接收add作为参数
  • 创建并返回judge函数,该函数通过闭包记住了add函数

2. 第一次调用:addCurry(1)

js 复制代码
const step1 = addCurry(1);
  • 传入第一个参数1,args = [1]
  • args.length(1) < add.length(3),不满足执行条件
  • 返回一个新函数:(...newArgs) => judge(1, ...newArgs)

3. 第二次调用:step1(2)

js 复制代码
const step2 = step1(2); 
  • 传入第二个参数2,args = [1, 2]
  • args.length(2) < add.length(3),仍不满足
  • 返回新函数:(...newArgs) => judge(1, 2, ...newArgs)

4. 第三次调用:step2(3)

js 复制代码
const result = step2(3);
  • 传入第三个参数3,args = [1, 2, 3]
  • args.length(3) === add.length(3),满足执行条件
  • 执行add(1, 2, 3)并返回结果6

介绍该实现过程中的闭包应用

闭包机制

讲讲这里闭包的形成

js 复制代码
function curry(fn) {  // 外部函数
    let judge = (...args) => {  // 内部函数
        
        if (args.length === fn.length) {// 由于闭包,可以访问fn
            return fn(...args);  //由于闭包可以访问args
        }
        return (...newArgs) => judge(...args, ...newArgs);  //由于闭包可以访问args
    }
    return judge;  // 返回内部函数
}

curry的fn闭包: 当执行const addCurry = curry(add)时:

  1. curry函数执行,创建了一个作用域,其中包含:

    • 参数fn(指向add函数)
    • 局部函数judge
  2. curry返回judge函数,但judge函数仍然保持着对fn的引用

  3. 即使curry函数执行完毕,其作用域也不会被销毁,因为judge函数还在引用其中的fn变量

递归时的闭包: 当柯里化函数被多次调用时,会形成闭包链:

js 复制代码
addCurry(1)(2)(3);
  1. addCurry(1)

    • 创建新作用域,args = [1]
    • 返回新函数,该函数闭包包含了args和原始fn
  2. (2)

    • 创建新作用域,newArgs = [2]
    • 合并参数[1, 2]
    • 返回新函数,闭包包含了合并后的参数和原始fn
  3. (3)

    • 参数足够,调用原始fn(1, 2, 3)

柯里化在 JS 中的应用

最后讲讲柯里化在JS中的应用!柯里化在 JavaScript 中有多种实用场景:

  1. 参数复用
js 复制代码
const add5 = curriedAdd(5);
console.log(add5(10)(15)); // 30
  1. 延迟执行
js 复制代码
// 先收集参数,需要时再执行
const fetchData = curry((url, params) => {
    // 实际请求逻辑
});

const getUser = fetchData('/api/user');
// 需要时再传入剩余参数
getUser({ id: 123 });
  1. 函数组合
js 复制代码
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

const toUpperCase = str => str.toUpperCase();
const exclaim = str => str + '!';

const shout = compose(exclaim, toUpperCase);
console.log(shout('hello')); // "HELLO!"
  1. React 高阶组件
js 复制代码
const withProps = curry((props, Component) => {
    return (ownProps) => <Component {...ownProps} {...props} />;
});

const withUser = withProps({ user: currentUser });
const EnhancedComponent = withUser(MyComponent);

总结

柯里化是一种强大的函数式编程技术,它通过将多参数函数转换为一系列单参数函数,提供了更大的灵活性和可组合性。在 JavaScript 中,我们可以利用闭包和递归优雅地实现柯里化,并在各种场景中应用它:

  • 参数复用和部分应用
  • 延迟函数执行
  • 函数组合和管道
  • 创建更专门化的函数版本

虽然柯里化可能带来一些性能上的考虑(每次调用都会创建新的函数),但在大多数应用中,它带来的代码清晰度和可维护性的提升远远超过了这点微小的性能开销。掌握柯里化可以让你写出更加声明式、模块化的 JavaScript 代码。

🌇结尾

感谢你看到最后,最后再说两点~

①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。

②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关推荐
zwjapple1 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20203 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem4 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊4 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术4 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing4 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止4 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall4 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴4 小时前
简单入门Python装饰器
前端·python
袁煦丞5 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作