字数 1963,阅读大约需 10 分钟
今天我们来聊聊JavaScript里的函数式编程。
你可能听过这个词,感觉它很高深。其实,它的核心思想很简单。函数式编程就是教我们怎么找到这些好用的“零件”,以及怎么把它们拼在一起。
一、它到底是什么?
我们先看一个最普通的代码。你想把一组数字都加上10,通常会这样写:
let numbers = [1, 2, 3, 4, 5];
let results = [];
for (let i = 0; i < numbers.length; i++) {
results.push(numbers[i] + 10);
}
console.log(results); // [11, 12, 13, 14, 15]
这段代码没问题,能完成任务。但它有几个小麻烦:
-
1. 我们创建了一个新数组 results,专门用来装结果。 -
2. 我们手动管理了循环计数器 i。 -
3. 我们明确地告诉计算机“怎么做”:循环、取值、加10、放入新数组。
函数式编程的思路不一样。它更关心“做什么”。上面的任务,用函数式的方法写出来是这样的:
let numbers = [1, 2, 3, 4, 5];
let results = numbers.map(num => num + 10);
console.log(results); // [11, 12, 13, 14, 15]
看,代码变短了,也变清晰了。我们直接对数组说:“请你把里面的每个元素都映射(map)成一个新值,新值是旧值加10。”我们不用管循环怎么跑,不用管中间变量,只声明了我们要的转换规则。
所以,函数式编程是一种编程范式。它把计算过程看作是数学函数的求值,避免改变状态和使用可变数据。 这句话有点绕,我们拆开看它的几个核心特点。
二、三个核心
函数式编程建立在几个重要的概念上。理解了它们,你就理解了大部分内容。
1. 纯函数
纯函数是函数式编程的基石.
一个纯函数有两个要求:
-
• 相同的输入,永远得到相同的输出。 -
• 没有副作用。 意思是它不会改变函数外部的任何东西(比如修改全局变量、改变传入的参数)。
我们看例子:
// 不纯的函数
let taxRate = 0.1; // 依赖外部变量
function calculateTax(price) {
return price * taxRate; // 如果taxRate变了,同样的price会得到不同结果
}
// 纯的函数
function calculateTaxPure(price, rate) {
return price * rate; // 结果只由参数决定,不影响任何外部状态
}
纯函数的好处太多了:
-
• 好测试:你不用设置一堆环境,给输入,断言输出就行。 -
• 好理解:你看函数签名就知道它能干什么,不用担心它暗地里搞小动作。 -
• 好复用:它不依赖特定上下文,搬到哪都能用。 -
• 好缓存:如果输入一样,我们可以直接把上次的结果给你,不用再算。
写代码时,多写纯函数,你的程序会稳定很多。
2. 不可变性
在函数式编程里,我们不修改已有的数据。如果想改变数据,我们就创建一份新的。
比如,我们有一个用户对象:
let user = { name: '小明', age: 20 };
传统做法可能是直接改:
user.age = 21; // 小明长大了
函数式做法是创建新对象:
let updatedUser = { ...user, age: 21 }; // 使用扩展运算符创建新对象
user 还是20岁,updatedUser 是21岁。原来的数据没动。
这样做有什么好处?最大的好处是安全。当数据不可变时,你就不用担心它在某个角落被意外修改,导致难以追踪的bug。尤其是在多线程或异步环境下,不可变数据能避免很多头疼的竞争问题。在JavaScript这种单线程语言里,它也让你对数据流更有把握。
数组操作也一样。不要用 push、pop、splice 去修改原数组,而是用 map、filter、slice、concat 这些返回新数组的方法。
3. 函数可以当“值”用
在JavaScript里,函数和其他值(数字、字符串)没什么不同。你可以:
-
• 把函数赋值给变量 -
• 把函数当作参数传给另一个函数 -
• 让一个函数返回另一个函数
这听起来平常,但威力巨大。它允许我们进行“高阶函数”操作。
高阶函数:要么接收函数作为参数,要么返回一个函数,要么两者都是。
我们之前用的 map 就是一个高阶函数,它接收一个函数作为参数。setTimeout 和 addEventListener 也是,它们都接收一个回调函数。
// 函数当参数
setTimeout(() => console.log('时间到!'), 1000);
// 函数当返回值
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
let double = createMultiplier(2);
console.log(double(5)); // 10
能轻松地操作函数,是我们组合功能、构建复杂逻辑的关键。
三、组合与柯里化
掌握了基础,我们可以玩点更厉害的。
函数组合
把多个小函数,组合成一个复杂的新函数。就像流水线,数据从一个函数流向下一个函数。
假设我们有两个简单的函数:
const add = (a, b) => a + b;
const square = x => x * x;
我们想先加再平方。可以手动组合:
let result = square(add(2, 3)); // (2+3)的平方 = 25
我们可以写一个通用的组合函数:
function compose(f, g) {
return function(x) {
return f(g(x));
};
}
// 或者用箭头函数更酷
const compose = (f, g) => x => f(g(x));
const addThenSquare = compose(square, add);
// 注意:add接收两个参数,这里需要特殊处理,仅作概念演示
组合让复杂逻辑由简单部件搭建而成,每个部件都容易测试和理解。
柯里化
把一个接收多个参数的函数,变成一系列接收一个参数的函数。
比如,一个普通的加法函数:
function add(a, b) {
return a + b;
}
add(2, 3); // 5
柯里化之后:
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
// 或者用箭头函数
const curriedAdd = a => b => a + b;
let add2 = curriedAdd(2); // 固定第一个参数为2
add2(3); // 5
add2(10); // 12
柯里化的好处是参数复用和延迟执行。你先提供一部分参数,得到一个更具体的函数,以后再用。这在组合函数时特别有用,因为它让每个函数都变成只接收一个参数的形式,更容易被组合。
四、一个例子
我们来看一个更贴近实际的例子。假设我们有一组用户数据,需要:
-
1. 筛选出活跃用户。 -
2. 获取他们的名字。 -
3. 生成欢迎邮件内容。
命令式写法(传统写法):
let users = [
{ name: '张三', active: true, email: 'zhang@example.com' },
{ name: '李四', active: false, email: 'li@example.com' },
{ name: '王五', active: true, email: 'wang@example.com' }
];
let activeUserNames = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
activeUserNames.push(users[i].name);
}
}
let messages = [];
for (let i = 0; i < activeUserNames.length; i++) {
messages.push(`欢迎回来,${activeUserNames[i]}!`);
}
console.log(messages);
函数式写法:
let users = [
{ name: '张三', active: true, email: 'zhang@example.com' },
{ name: '李四', active: false, email: 'li@example.com' },
{ name: '王五', active: true, email: 'wang@example.com' }
];
let messages = users
.filter(user => user.active) // 第一步:过滤
.map(user => user.name) // 第二步:提取名字
.map(name => `欢迎回来,${name}!`); // 第三步:生成消息
console.log(messages);
// ["欢迎回来,张三!", "欢迎回来,王五!"]
哪个更清晰?哪个更容易修改?
最后
函数式编程不是魔法,它是一套经过时间考验的、让代码变得更清晰、更稳定的思想和工具。
它不要求你重写整个项目,而是鼓励你从下一个函数、下一段数据处理开始尝试。

