跳到主要内容

高阶函数

高阶函数(Higher-Order Functions)是函数式编程的核心概念之一。理解高阶函数是掌握函数式编程的关键步骤,因为它们是函数组合、抽象和复用的基础。

什么是高阶函数?

高阶函数是指满足以下任一条件的函数:

  1. 接受一个或多个函数作为参数
  2. 返回一个函数作为结果

这个定义听起来简单,但它蕴含着深刻的编程思想。在函数式编程中,函数被视为"一等公民"(First-Class Citizen),这意味着函数可以像数字、字符串一样被传递、存储和操作。高阶函数正是利用这一特性,将函数本身作为构建块来创建更复杂的逻辑。

为什么高阶函数重要?

传统的过程式编程中,我们操作的是数据:数字、字符串、数组等。而高阶函数让我们能够操作"行为"本身。这种抽象层次的提升带来了几个关键优势:

  • 抽象控制流:高阶函数可以将通用的控制模式(如遍历、过滤、转换)抽象出来
  • 代码复用:通过传入不同的函数,同一个高阶函数可以实现多种不同的行为
  • 声明式编程:描述"做什么"而不是"怎么做",代码更易理解
  • 组合性:小函数可以像积木一样组合成复杂的功能

接受函数作为参数

这是高阶函数最常见的用法。通过将函数作为参数传入,我们可以让同一个高阶函数表现出不同的行为。

内置高阶函数

JavaScript 数组提供了多个内置的高阶函数,它们是学习高阶函数的最佳起点。

map - 转换每个元素

map 接受一个转换函数,将该函数应用于数组的每个元素,返回一个新数组。它不修改原数组,这符合函数式编程的不可变原则。

const numbers = [1, 2, 3, 4, 5];

// map 接受一个函数,将每个元素乘以 2
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// 原数组不变
console.log(numbers); // [1, 2, 3, 4, 5]

// 转换对象数组
const users = [
{ name: '张三', age: 25 },
{ name: '李四', age: 30 }
];

const names = users.map(user => user.name);
console.log(names); // ['张三', '李四']

// 更复杂的转换
const userDescriptions = users.map(user => `${user.name}${user.age}`);
console.log(userDescriptions); // ['张三,25岁', '李四,30岁']

map 的核心思想是:我告诉你如何转换一个元素,map 负责把这种转换应用到所有元素上。你不需要写循环,不需要管理索引,只需专注于"转换"本身。

filter - 过滤元素

filter 接受一个谓词函数(返回布尔值的函数),保留使谓词返回 true 的元素。

const numbers = [1, 2, 3, 4, 5, 6];

// 保留偶数
const evens = numbers.filter(x => x % 2 === 0);
console.log(evens); // [2, 4, 6]

// 保留大于 3 的数
const greaterThanThree = numbers.filter(x => x > 3);
console.log(greaterThanThree); // [4, 5, 6]

// 过滤对象数组
const products = [
{ name: '苹果', price: 5, inStock: true },
{ name: '香蕉', price: 3, inStock: false },
{ name: '橙子', price: 4, inStock: true }
];

// 只保留有库存且价格低于 5 的商品
const available = products.filter(p => p.inStock && p.price < 5);
console.log(available);
// [{ name: '橙子', price: 4, inStock: true }]

filter 的语义非常清晰:给定一个条件,给我所有满足条件的元素。这比手动写循环和 if 语句更直观。

reduce - 归约累积

reduce 是最强大的数组方法,它可以将数组"归约"为单个值。它接受一个累积器函数和一个初始值,依次处理每个元素。

const numbers = [1, 2, 3, 4, 5];

// 求和
const sum = numbers.reduce((acc, x) => acc + x, 0);
console.log(sum); // 15

// 理解 reduce 的执行过程:
// 初始值 acc = 0
// 第1次: acc = 0, x = 1 → acc + x = 1
// 第2次: acc = 1, x = 2 → acc + x = 3
// 第3次: acc = 3, x = 3 → acc + x = 6
// 第4次: acc = 6, x = 4 → acc + x = 10
// 第5次: acc = 10, x = 5 → acc + x = 15
// 最终结果: 15

// 求最大值
const max = numbers.reduce((acc, x) => x > acc ? x : acc, -Infinity);
console.log(max); // 5

// 统计字符出现次数
const chars = ['a', 'b', 'a', 'c', 'b', 'a'];
const count = chars.reduce((acc, char) => {
acc[char] = (acc[char] || 0) + 1;
return acc;
}, {});
console.log(count); // { a: 3, b: 2, c: 1 }

// 数组分组
const people = [
{ name: '张三', age: 25 },
{ name: '李四', age: 30 },
{ name: '王五', age: 25 }
];

const byAge = people.reduce((acc, person) => {
const key = person.age;
if (!acc[key]) acc[key] = [];
acc[key].push(person);
return acc;
}, {});

console.log(byAge);
// {
// 25: [{ name: '张三', age: 25 }, { name: '王五', age: 25 }],
// 30: [{ name: '李四', age: 30 }]
// }

reduce 的强大之处在于它的通用性。实际上,mapfilter 都可以用 reduce 实现:

// 用 reduce 实现 map
const map = (arr, fn) => arr.reduce((acc, x) => [...acc, fn(x)], []);

// 用 reduce 实现 filter
const filter = (arr, predicate) =>
arr.reduce((acc, x) => predicate(x) ? [...acc, x] : acc, []);

find 和 findIndex - 查找元素

const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
];

// 查找符合条件的第一个元素
const user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: '李四' }

// 查找符合条件的第一个元素的索引
const index = users.findIndex(u => u.name === '王五');
console.log(index); // 2

// 如果没找到
const notFound = users.find(u => u.id === 999);
console.log(notFound); // undefined

some 和 every - 条件判断

const numbers = [1, 2, 3, 4, 5];

// some: 是否存在满足条件的元素
const hasEven = numbers.some(x => x % 2 === 0);
console.log(hasEven); // true

const hasNegative = numbers.some(x => x < 0);
console.log(hasNegative); // false

// every: 是否所有元素都满足条件
const allPositive = numbers.every(x => x > 0);
console.log(allPositive); // true

const allGreaterThanTwo = numbers.every(x => x > 2);
console.log(allGreaterThanTwo); // false

自定义接受函数的高阶函数

除了内置方法,我们经常需要自己创建接受函数的高阶函数:

// 一个通用的数据处理函数
function processArray(array, transformer, filter) {
return array
.filter(filter)
.map(transformer);
}

const numbers = [1, 2, 3, 4, 5, 6];

// 使用不同的函数获得不同结果
const result1 = processArray(
numbers,
x => x * x, // 平方
x => x % 2 === 0 // 只处理偶数
);
console.log(result1); // [4, 16, 36]

const result2 = processArray(
numbers,
x => x + 10, // 加 10
x => x > 3 // 只处理大于 3 的数
);
console.log(result2); // [14, 15, 16]

// 带条件的执行函数
function when(condition, fn) {
return function(value) {
return condition(value) ? fn(value) : value;
};
}

const processNumber = when(
x => x > 0, // 条件:正数
x => x * 2 // 满足条件时:乘以 2
);

console.log(processNumber(5)); // 10
console.log(processNumber(-3)); // -3(不满足条件,原样返回)

返回函数的高阶函数

返回函数的高阶函数是创建可配置、可复用函数的强大工具。这种技术被称为"函数工厂"。

创建配置化的函数

// 创建乘法器
function multiplier(factor) {
return function(number) {
return number * factor;
};
}

// 创建特定的乘法函数
const double = multiplier(2);
const triple = multiplier(3);
const tenTimes = multiplier(10);

console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenTimes(5)); // 50

// 创建问候函数
function greeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}

const sayHello = greeter('你好');
const sayGoodbye = greeter('再见');

console.log(sayHello('张三')); // 你好, 张三!
console.log(sayGoodbye('李四')); // 再见, 李四!

// 创建带精度的舍入函数
function roundTo(precision) {
const factor = Math.pow(10, precision);
return function(number) {
return Math.round(number * factor) / factor;
};
}

const roundTo2 = roundTo(2);
const roundTo4 = roundTo(4);

console.log(roundTo2(3.14159)); // 3.14
console.log(roundTo4(3.14159)); // 3.1416

创建验证函数

// 通用的验证器创建函数
function createValidator(predicate, message) {
return function(value) {
if (predicate(value)) {
return { valid: true, value };
}
return { valid: false, error: message };
};
}

// 创建具体的验证器
const isEmail = createValidator(
value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
'请输入有效的邮箱地址'
);

const isNotEmpty = createValidator(
value => value && value.trim() !== '',
'此字段不能为空'
);

const isLength = min => createValidator(
value => value.length >= min,
`长度至少需要 ${min} 个字符`
);

// 使用
console.log(isEmail('[email protected]')); // { valid: true, value: '[email protected]' }
console.log(isEmail('invalid')); // { valid: false, error: '请输入有效的邮箱地址' }
console.log(isNotEmpty('hello')); // { valid: true, value: 'hello' }
console.log(isNotEmpty('')); // { valid: false, error: '此字段不能为空' }

const isMinLength8 = isLength(8);
console.log(isMinLength8('password')); // { valid: true, value: 'password' }
console.log(isMinLength8('pass')); // { valid: false, error: '长度至少需要 8 个字符' }

包装器函数

返回函数的高阶函数可以用来"包装"其他函数,添加额外行为:

// 添加日志功能
function withLogging(fn) {
return function(...args) {
console.log(`调用函数,参数: ${JSON.stringify(args)}`);
const result = fn.apply(this, args);
console.log(`函数返回: ${JSON.stringify(result)}`);
return result;
};
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);

loggedAdd(2, 3);
// 调用函数,参数: [2,3]
// 函数返回: 5

// 添加计时功能
function withTiming(fn) {
return function(...args) {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`执行耗时: ${(end - start).toFixed(2)}ms`);
return result;
};
}

// 添加重试功能
function withRetry(fn, maxRetries = 3) {
return async function(...args) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn.apply(this, args);
} catch (error) {
lastError = error;
console.log(`${i + 1} 次尝试失败,正在重试...`);
}
}
throw lastError;
};
}

// 组合多个包装器
const fetchWithLogging = withLogging(fetch);
const fetchWithRetry = withRetry(fetchWithLogging, 3);

记忆化函数

记忆化是一种优化技术,缓存函数的计算结果:

function memoize(fn) {
const cache = new Map();

return function(...args) {
// 创建缓存键(处理引用类型参数)
const key = JSON.stringify(args);

// 命中缓存
if (cache.has(key)) {
console.log('从缓存返回');
return cache.get(key);
}

// 计算并缓存
console.log('计算中...');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}

// 计算斐波那契数
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}

// 这个版本很慢,因为会重复计算
console.time('no memo');
console.log(fibonacci(35));
console.timeEnd('no memo');

// 使用记忆化优化
const memoFib = memoize(function fib(n) {
if (n <= 1) return n;
return memoFib(n - 1) + memoFib(n - 2);
});

console.time('with memo');
console.log(memoFib(35));
console.timeEnd('with memo');

同时接受和返回函数

最强大的高阶函数既接受函数作为参数,又返回函数作为结果。这种模式常见于函数组合和中间件。

// 函数组合
function compose(f, g) {
return function(x) {
return f(g(x));
};
}

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

const addOneThenDouble = compose(double, addOne);
console.log(addOneThenDouble(5)); // 12 (先 +1 得 6,再 ×2 得 12)

// 组合多个函数
function composeAll(...fns) {
return function(x) {
return fns.reduceRight((acc, fn) => fn(acc), x);
};
}

const process = composeAll(
x => x + 1,
x => x * 2,
x => x - 3
);
console.log(process(5)); // 5 → 2 → 4 → 5

// 管道(从左到右)
function pipe(...fns) {
return function(x) {
return fns.reduce((acc, fn) => fn(acc), x);
};
}

const pipeline = pipe(
x => x - 3,
x => x * 2,
x => x + 1
);
console.log(pipeline(5)); // 5 → 2 → 4 → 5

高阶函数的优势

1. 抽象和复用

高阶函数将通用的模式抽象出来,避免重复代码:

// 不使用高阶函数
function processNumbers(numbers) {
const result = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] > 0) {
result.push(numbers[i] * 2);
}
}
return result;
}

// 使用高阶函数
const processNumbers = numbers =>
numbers.filter(x => x > 0).map(x => x * 2);

2. 声明式编程

高阶函数让代码更接近问题的描述:

// 命令式:描述怎么做
const activeUserNames = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
activeUserNames.push(users[i].name);
}
}

// 声明式:描述要什么
const activeUserNames = users
.filter(u => u.active)
.map(u => u.name);

3. 可组合性

高阶函数可以像管道一样组合:

const users = [
{ name: '张三', age: 25, active: true },
{ name: '李四', age: 30, active: false },
{ name: '王五', age: 35, active: true }
];

const result = users
.filter(u => u.active) // 只保留活跃用户
.map(u => ({ ...u, ageGroup: u.age >= 30 ? 'senior' : 'junior' })) // 添加年龄分组
.sort((a, b) => a.age - b.age) // 按年龄排序
.map(u => u.name); // 只取名字

console.log(result); // ['张三', '王五']

4. 惰性求值和无限序列

返回函数的高阶函数可以创建惰性序列:

function* naturalNumbers() {
let n = 0;
while (true) {
yield n++;
}
}

function take(n, iterable) {
const result = [];
for (const item of iterable) {
if (result.length >= n) break;
result.push(item);
}
return result;
}

const firstTen = take(10, naturalNumbers());
console.log(firstTen); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

实践建议

1. 优先使用内置高阶函数

对于数组的常见操作,优先使用 mapfilterreduce 等内置方法:

// 避免
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}

// 推荐
const doubled = numbers.map(x => x * 2);

2. 提取重复逻辑为高阶函数

当发现相似的代码模式时,考虑抽象为高阶函数:

// 多处相似的异步错误处理
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (error) {
console.error('获取用户失败:', error);
throw error;
}
}

async function fetchPost(id) {
try {
const response = await fetch(`/api/posts/${id}`);
return await response.json();
} catch (error) {
console.error('获取文章失败:', error);
throw error;
}
}

// 抽象为高阶函数
function createFetcher(resource, errorPrefix) {
return async function(id) {
try {
const response = await fetch(`/api/${resource}/${id}`);
return await response.json();
} catch (error) {
console.error(`${errorPrefix}:`, error);
throw error;
}
};
}

const fetchUser = createFetcher('users', '获取用户失败');
const fetchPost = createFetcher('posts', '获取文章失败');

3. 注意纯函数和副作用

在高阶函数的回调中,尽量保持纯函数,避免副作用:

// 有副作用 - 不推荐
let total = 0;
numbers.forEach(n => {
total += n;
});

// 纯函数 - 推荐
const total = numbers.reduce((acc, n) => acc + n, 0);

4. 合理使用箭头函数

对于简单操作,箭头函数让代码更简洁:

// 简单操作
const doubled = numbers.map(x => x * 2);

// 复杂操作,使用函数体增加可读性
const processed = users.map(user => {
const fullName = `${user.firstName} ${user.lastName}`;
const age = calculateAge(user.birthDate);
return { ...user, fullName, age };
});

小结

高阶函数是函数式编程的核心工具:

  • 接受函数作为参数:实现行为的参数化,如 mapfilterreduce
  • 返回函数作为结果:创建可配置的函数工厂,如 multipliermemoize
  • 同时接受和返回:实现函数组合和装饰器模式

掌握高阶函数需要理解:

  1. 函数是一等公民,可以像数据一样传递
  2. 将行为抽象为函数,提高代码复用性
  3. 通过组合小函数构建复杂功能

高阶函数让代码更具表达力、更易于测试、更容易组合。它们是通向函数式编程思维的关键一步。