跳到主要内容

函数式编程基础

函数式编程建立在几个核心概念之上。理解这些概念是掌握函数式编程的关键。

纯函数

纯函数是函数式编程的基石。一个函数是纯函数,当且仅当:

  1. 相同的输入总是产生相同的输出(确定性)
  2. 没有副作用(不影响外部状态)

示例

// 纯函数
function add(a, b) {
return a + b;
}

// 每次调用 add(2, 3) 总是返回 5
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5

// 非纯函数(依赖外部状态)
let taxRate = 0.1;
function calculateTax(amount) {
return amount * taxRate; // 依赖外部变量 taxRate
}

// 非纯函数(有副作用)
function appendToList(item, list) {
list.push(item); // 修改了输入参数
return list;
}

为什么纯函数重要?

纯函数有以下优点:

  1. 易于测试:不需要复杂的测试环境
  2. 易于推理:函数的行为完全由输入决定
  3. 可缓存:相同输入可以缓存结果(记忆化)
  4. 可并行化:没有共享状态,天然支持并发
// 纯函数的缓存(记忆化)
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) {
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}

// 缓存计算密集型函数
const memoizedAdd = memoize((a, b) => {
console.log('计算中...');
return a + b;
});

console.log(memoizedAdd(1, 2)); // 计算中... → 3
console.log(memoizedAdd(1, 2)); // 3(直接从缓存返回)

副作用

副作用是指函数除了返回值之外,还对外部状态产生了影响。常见的副作用包括:

  • 修改全局变量或外部变量
  • 修改输入参数
  • 进行 I/O 操作(文件读写、网络请求)
  • 打印日志
  • 修改 DOM
  • 触发事件
// 有副作用的函数
let counter = 0;

function increment() {
counter++; // 修改外部状态
console.log(counter); // I/O 操作
return counter;
}

// 无副作用的函数(纯函数)
function incrementPure(value) {
return value + 1;
}

隔离副作用

在实际应用中,副作用是不可避免的(程序需要与外部世界交互)。函数式编程通过将副作用隔离到特定区域来管理它们:

// 将副作用推到程序的边界
function processUserData(users) {
// 纯函数部分:数据处理
const filtered = users.filter(user => user.active);
const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
const formatted = sorted.map(user => `${user.name} (${user.email})`);

return formatted;
}

// 在边界处理副作用
function main() {
// 获取数据(副作用)
const users = fetchUsersFromAPI();

// 处理数据(纯函数)
const result = processUserData(users);

// 输出结果(副作用)
result.forEach(line => console.log(line));
}

引用透明

引用透明是指一个表达式可以被它的值替换而不改变程序的行为。纯函数保证了引用透明。

// 引用透明示例
const result1 = add(2, 3) * 2; // 10
const result2 = 5 * 2; // 10(add(2, 3) 被替换为 5)

// 非引用透明
let x = 0;
function increment() {
return ++x;
}

const result3 = increment() + increment(); // 3(1 + 2)
const result4 = 1 + 2; // 3(但行为不同)

不可变性

不可变性是指数据一旦创建就不能被修改。在函数式编程中,我们通过创建新数据而不是修改现有数据来处理变化。

// 可变操作(非函数式)
const arr = [1, 2, 3];
arr.push(4); // 修改了原数组
arr[0] = 0; // 修改了原数组

// 不可变操作(函数式)
const original = [1, 2, 3];
const newArr = [...original, 4]; // 创建新数组
const modified = [0, ...original.slice(1)]; // 创建新数组

console.log(original); // [1, 2, 3](未改变)
console.log(newArr); // [1, 2, 3, 4]
console.log(modified); // [0, 2, 3]

Object.freeze 的局限

Object.freeze 只能进行浅冻结,不能完全保证不可变性:

// 浅冻结
const frozen = Object.freeze({ name: '张三', address: { city: '北京' } });
frozen.name = '李四'; // 静默失败或报错(严格模式)
frozen.address.city = '上海'; // 可以修改!(嵌套对象未冻结)

// 深冻结函数
function deepFreeze(obj) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key]);
}
});
return Object.freeze(obj);
}

一等函数

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

  1. 被赋值给变量
  2. 作为参数传递
  3. 作为返回值返回
  4. 存储在数据结构中
// 函数赋值给变量
const greet = function(name) {
return `你好,${name}`;
};

// 函数作为参数
function callWithHello(fn) {
return fn('Hello');
}

console.log(callWithHello(x => x.toUpperCase())); // "HELLO"

// 函数作为返回值
function multiplier(factor) {
return function(number) {
return number * factor;
};
}

const double = multiplier(2);
console.log(double(5)); // 10

// 函数存储在数组中
const operations = [
x => x + 1,
x => x * 2,
x => x - 3
];

const result = operations.reduce((acc, fn) => fn(acc), 5);
console.log(result); // ((5 + 1) * 2) - 3 = 9

高阶函数初探

高阶函数是接收函数作为参数或返回函数的函数。JavaScript 内置了许多高阶函数:

// Array.prototype.map - 转换每个元素
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// Array.prototype.filter - 过滤元素
const evens = numbers.filter(x => x % 2 === 0);
console.log(evens); // [2, 4]

// Array.prototype.reduce - 累积计算
const sum = numbers.reduce((acc, x) => acc + x, 0);
console.log(sum); // 15

// Array.prototype.find - 查找元素
const found = numbers.find(x => x > 3);
console.log(found); // 4

函数组合示例

函数组合是将多个函数组合在一起,形成一个新的函数:

// 简单的函数组合
const compose = (f, g) => x => f(g(x));

const addOne = x => x + 1;
const double = x => x * 2;

const doubleThenAddOne = compose(addOne, double);
console.log(doubleThenAddOne(5)); // 11(double(5) = 10,addOne(10) = 11)

// 管道(从左到右执行)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

const transform = pipe(
x => x + 1,
x => x * 2,
x => x - 3
);

console.log(transform(5)); // 9(5 + 1 = 6,6 * 2 = 12,12 - 3 = 9)

小结

本章我们学习了函数式编程的核心概念:

  1. 纯函数:相同输入产生相同输出,没有副作用
  2. 副作用:对外部状态的影响,需要隔离管理
  3. 引用透明:表达式可以被其值替换
  4. 不可变性:数据不可修改,通过创建新数据处理变化
  5. 一等函数:函数可以作为值传递
  6. 高阶函数:接收或返回函数的函数
  7. 函数组合:将小函数组合成复杂功能

练习

  1. 判断以下函数是否是纯函数,并说明原因:

    function square(x) {
    return x * x;
    }

    let total = 0;
    function addToTotal(x) {
    total += x;
    return total;
    }
  2. 使用纯函数重写以下函数:

    function appendAndSort(item, arr) {
    arr.push(item);
    return arr.sort();
    }
  3. 实现一个 pipe 函数,将多个函数从左到右组合。

  4. 使用 mapfilterreduce 计算数组中所有偶数的平方和。