跳到主要内容

JavaScript 函数柯里化

什么是函数柯里化?

函数柯里化(Currying)是一种函数式编程技术,它是以数学家哈斯凯尔·柯里(Haskell Curry)命名的。简单来说,柯里化是将一个接受多个参数的函数转换为一系列使用一个参数的函数的过程。

备注

柯里化不会调用函数,它只是对函数进行转换。

在JavaScript中,柯里化后的函数不再一次接收所有参数,而是返回一个新函数,这个新函数接收下一个参数,依此类推,直至收到所有参数后,返回最终结果。

函数柯里化的基本形式

假设有一个函数 add,接收三个参数并返回它们的和:

javascript
// 普通函数
function add(a, b, c) {
return a + b + c;
}

console.log(add(1, 2, 3)); // 输出: 6

柯里化后的版本如下:

javascript
// 柯里化后的函数
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}

console.log(curriedAdd(1)(2)(3)); // 输出: 6

每调用一次,都会返回一个新函数,直到最后一个参数被传入,才会计算并返回最终结果。

ES6箭头函数简化柯里化

使用ES6的箭头函数可以使柯里化函数的代码更加简洁:

javascript
const curriedAdd = a => b => c => a + b + c;
console.log(curriedAdd(1)(2)(3)); // 输出: 6

创建通用的柯里化函数

我们可以编写一个通用的柯里化函数,它接收一个函数并返回这个函数的柯里化版本:

javascript
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}

// 使用通用柯里化函数
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

这个通用柯里化函数允许我们以多种方式传递参数,只要最终传递的参数数量等于原始函数所需的参数数量即可。

函数柯里化的优点

1. 参数复用

柯里化允许我们固定某些参数,创建一个新的函数,这对于代码复用非常有用:

javascript
function discount(discount) {
return function(price) {
return price * (1 - discount);
};
}

const tenPercentDiscount = discount(0.1);
const twentyPercentDiscount = discount(0.2);

console.log(tenPercentDiscount(100)); // 输出: 90
console.log(twentyPercentDiscount(100)); // 输出: 80

2. 延迟执行

柯里化可以帮助我们延迟函数的执行,直到收集到所有需要的数据:

javascript
const loggerHelper = format => date => message => {
console.log(`[${format(date)}] ${message}`);
};

const timeFormat = date => `${date.getHours()}:${date.getMinutes()}`;
const simpleFormat = date => `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`;

const timeLogger = loggerHelper(timeFormat);
const todayLogger = timeLogger(new Date());

todayLogger("Function currying is awesome!");
// 输出格式例如: [14:25] Function currying is awesome!

3. 提高可读性

对于一些特定类型的代码,柯里化可以使代码更加可读和组合性更强:

javascript
// 未柯里化的过滤函数
function filter(arr, predicate) {
return arr.filter(predicate);
}

// 柯里化版本
const curriedFilter = predicate => arr => arr.filter(predicate);

const isEven = x => x % 2 === 0;
const getEvenNumbers = curriedFilter(isEven);

console.log(getEvenNumbers([1, 2, 3, 4, 5, 6])); // 输出: [2, 4, 6]

实际应用场景

1. 事件处理

柯里化在事件处理中特别有用,可以预先设置一些参数:

javascript
const handleEvent = eventType => element => callback => {
element.addEventListener(eventType, callback);
return {
remove: () => element.removeEventListener(eventType, callback)
};
};

const handleClick = handleEvent('click');
const handleDocumentClick = handleClick(document);

const documentClickHandler = handleDocumentClick(e => {
console.log('Document was clicked', e);
});

// 移除事件监听器
// documentClickHandler.remove();

2. 数据验证

柯里化可用于创建可复用的验证函数:

javascript
const validate = validationFn => field => errorMessage => value => {
if (validationFn(value)) {
return { valid: true, value };
} else {
return { valid: false, error: `${field} ${errorMessage}` };
}
};

const isNotEmpty = value => value !== '';
const validateNotEmpty = validate(isNotEmpty);
const validateUsername = validateNotEmpty('Username')('cannot be empty');

console.log(validateUsername(''));
// 输出: { valid: false, error: 'Username cannot be empty' }
console.log(validateUsername('user1'));
// 输出: { valid: true, value: 'user1' }

3. API请求构建

柯里化可以帮助构建灵活的API请求函数:

javascript
const fetchAPI = baseURL => endpoint => options => {
const url = `${baseURL}${endpoint}`;
return fetch(url, options)
.then(response => response.json())
.catch(error => console.error(`Error fetching ${url}:`, error));
};

const fetchFromMyAPI = fetchAPI('https://myapi.com');
const fetchUsers = fetchFromMyAPI('/users');

// 获取所有用户
const getUsers = () => fetchUsers({ method: 'GET' });

// 获取特定用户
const getUser = id => fetchFromMyAPI(`/users/${id}`)({ method: 'GET' });

函数柯里化的局限性

虽然柯里化有很多优点,但也存在一些局限性:

  1. 可读性:过度使用柯里化可能导致代码难以阅读和理解,尤其是对不熟悉函数式编程的开发者来说。

  2. 性能考虑:每次调用柯里化函数都会创建一个新的函数,可能带来额外的性能开销。

  3. 调试复杂性:柯里化函数的调试可能比普通函数更复杂,因为需要追踪多个嵌套函数的执行。

性能优化的柯里化

如果担心性能问题,可以使用函数记忆(memoization)来优化柯里化函数:

javascript
function memoizedCurry(fn) {
const cache = {};
return function curried(...args) {
const key = JSON.stringify(args);
if (args.length >= fn.length) {
if (cache[key]) {
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}

const expensiveAdd = (a, b, c) => {
console.log('Calculating...');
return a + b + c;
};

const memoizedAdd = memoizedCurry(expensiveAdd);

console.log(memoizedAdd(1)(2)(3)); // 输出: Calculating... 6
console.log(memoizedAdd(1)(2)(3)); // 输出: 6 (从缓存中获取,不会再计算)

总结

函数柯里化是JavaScript函数式编程中的重要概念,它能让我们创建更灵活、可复用的函数。通过将多参数函数转换为一系列单参数函数,柯里化帮助我们实现:

  • 参数复用
  • 延迟执行
  • 代码组合
  • 更清晰的代码结构

虽然柯里化不适用于所有场景,但在恰当的情况下,它可以大大改善代码的质量和可维护性。

实践练习

尝试将以下函数柯里化:

  1. multiply(a, b, c) 返回三个数的乘积
  2. formatGreeting(greeting, name) 返回形如 "Hello, John!" 的字符串
  3. 创建一个通用的 pipe 函数,用于组合多个函数的执行

扩展阅读

要深入了解函数柯里化和函数式编程,可以参考以下资源:

  1. JavaScript函数式编程的其他概念,如纯函数、函数组合和不可变数据
  2. 函数式编程库如Ramda和Lodash/FP,它们提供了许多实用的柯里化函数
  3. 探索柯里化与偏函数应用(Partial Application)的区别和联系

通过掌握柯里化,你已经迈出了成为函数式编程高手的重要一步!