柯里化:从七龙珠到神龙召唤的编程魔法 🐉

269 阅读3分钟

大家好!今天我们来聊聊JavaScript中一个既神秘又有趣的概念——柯里化(Curry)。这可不是咖喱(Curry)那个好吃的食物哦,而是一种强大的函数式编程技术!

一、闭包:柯里化的基石

在深入柯里化之前,我们需要先理解闭包(Closure)这个概念。闭包就像是函数的小背包,可以随身携带一些"零食"(自由变量)。

function outer() {
    const snack = '🍪'; // 自由变量
    
    return function inner() {
        console.log(`我偷偷吃了个${snack}`);
    };
}

const eatSnack = outer();
eatSnack(); // 输出: 我偷偷吃了个🍪

这里inner函数就是一个闭包,它可以访问外层函数的snack变量,即使outer已经执行完毕。这个特性正是柯里化能够实现的关键!

二、函数是一等公民

在JavaScript中,函数是一等公民(first-class citizen),这意味着:

  1. 函数可以赋值给变量
  2. 函数可以作为参数传递
  3. 函数可以作为返回值
// 1. 赋值给变量
const sayHi = function() { console.log('Hi!'); };

// 2. 作为参数传递
function greet(fn) {
    fn();
}
greet(sayHi);

// 3. 作为返回值
function createGreeter() {
    return function() { console.log('Hello!'); };
}

这种灵活性让我们可以玩出各种花样,比如立即执行函数(IIFE)

(function() {
    console.log('我立即执行啦!');
})();

三、柯里化:七龙珠的召唤术

现在进入正题!柯里化的概念其实很简单:把接受多个参数的函数变成一系列接受单一参数的函数

想象一下《七龙珠》中的场景:要召唤神龙,需要集齐7颗龙珠。柯里化函数就像是在逐步收集龙珠(参数),当集齐所有龙珠(参数)时,就能召唤神龙(执行函数)!

1. 普通函数 vs 柯里化函数

// 普通加法函数
function add(a, b, c) {
    return a + b + c;
}
add(1, 2, 3); // 6

// 柯里化后的加法函数
function curriedAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}
curriedAdd(1)(2)(3); // 6

2. 自动柯里化的神奇函数

手动写柯里化函数太麻烦了,我们来写一个通用的柯里化函数

function curry(fn) {
    // 返回一个函数
    // ...args 收集所有参数(自由变量)
    let judge = (...args) => {
        // 如果参数数量足够,就执行原函数
        if (args.length === fn.length) {
            return fn(...args);
        }
        // 否则返回一个新函数继续收集参数
        return (...newArgs) => judge(...args, ...newArgs);
    };
    return judge;
}

使用这个魔法函数:

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

3. 柯里化的实际应用

场景1:参数复用

// 普通函数
function log(level, time, message) {
    console.log(`${level} ${time}: ${message}`);
}

// 柯里化后
const curriedLog = curry(log);
const logError = curriedLog('ERROR');
const logErrorToday = logError('2023-06-15');

logErrorToday('系统崩溃了!'); // ERROR 2023-06-15: 系统崩溃了!

场景2:延迟执行

const fetchData = curry(function(url, params, callback) {
    // 模拟API请求
    setTimeout(() => {
        callback(`从${url}获取数据: ${JSON.stringify(params)}`);
    }, 1000);
});

const getUser = fetchData('/api/user');
const getUserWithLimit = getUser({ limit: 10 });

getUserWithLimit(data => console.log(data));
// 1秒后输出: 从/api/user获取数据: {"limit":10}

四、柯里化的高级技巧

1. 无限参数柯里化

前面的柯里化函数需要知道参数数量,我们可以改进它:

function infiniteCurry(fn) {
    return function curried(...args) {
        // 如果没有参数传入,就执行函数
        if (args.length === 0) {
            return fn.apply(this, []);
        }
        return function(...newArgs) {
            if (newArgs.length === 0) {
                return fn.apply(this, args);
            }
            return curried.apply(this, args.concat(newArgs));
        };
    };
}

const sum = infiniteCurry((...nums) => nums.reduce((a, b) => a + b, 0));

console.log(sum(1)(2)(3)()); // 6
console.log(sum(1, 2)(3, 4)()); // 10

2. 反柯里化

有时候我们也需要把柯里化函数变回普通函数:

Function.prototype.uncurry = function() {
    return (...args) => {
        let fn = this;
        for (let arg of args) {
            fn = fn(arg);
        }
        return fn;
    };
};

const uncurriedAdd = curriedAdd.uncurry();
console.log(uncurriedAdd(1, 2, 3)); // 6

五、柯里化的性能考量

柯里化虽然强大,但也有一些需要注意的地方:

  1. 内存占用:每个柯里化层级都会创建一个新函数,可能增加内存使用
  2. 调用栈:深层嵌套的柯里化可能导致调用栈变深
  3. 可读性:过度使用柯里化可能降低代码可读性

建议在以下场景使用柯里化:

  • 需要部分应用参数时
  • 需要创建函数变体时
  • 函数组合和管道操作时

六、柯里化的趣味应用

1. 函数组合

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);

console.log(shout('hello')); // HELLO!

2. 验证器构建器

const createValidator = curry((predicate, errorMsg, value) => {
    return predicate(value) ? [] : [errorMsg];
});

const isRequired = createValidator(x => x !== '', '不能为空');
const isEmail = createValidator(
    x => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(x),
    '必须是有效的邮箱'
);

console.log(isRequired('')); // ["不能为空"]
console.log(isEmail('test@example.com')); // []

八、总结:柯里化的力量

柯里化就像是编程世界的乐高积木,它让我们能够:

  1. 分解复杂问题:将多参数函数拆解为单参数函数链
  2. 提高代码复用:通过部分应用创建新函数
  3. 增强函数组合能力:便于创建函数管道
  4. 延迟执行:只在需要时才真正执行

记住我们的七龙珠比喻:每个柯里化步骤就像收集一颗龙珠,当集齐所有龙珠(参数)时,就能召唤神龙(执行函数)!

希望这篇笔记能帮助你理解柯里化这个强大的概念。下次当你看到curry这个词时,不仅能想到美味的咖喱,还能想到这个强大的函数式编程技术!

Happy Coding! 🚀