Skip to content

高阶函数:将函数作为数据操作

回想一下工厂的生产线。有的机器生产零件,有的机器组装零件,还有一种特殊的机器——它不直接生产产品,而是控制其他机器如何工作。这种"元级别"的机器可以接收不同的工作指令,根据指令调整生产流程。在 JavaScript 中,高阶函数就是这样的"元级别"函数——它们可以接收函数作为参数,或者返回函数作为结果,从而实现更灵活、更抽象的编程。

什么是高阶函数

高阶函数(Higher-Order Function)是满足以下至少一个条件的函数:

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

这个概念来自数学和函数式编程,JavaScript 中的函数是"一等公民"(first-class citizens),意味着函数可以像其他值一样被传递、存储和操作。

让我们从最简单的例子开始:

javascript
// 一个普通函数
function sayHello() {
  console.log("Hello!");
}

// 一个高阶函数:接受函数作为参数
function executeFunction(fn) {
  console.log("About to execute the function...");
  fn(); // 调用传入的函数
  console.log("Function executed!");
}

executeFunction(sayHello);
// 输出:
// About to execute the function...
// Hello!
// Function executed!

在这个例子中,executeFunction 是一个高阶函数,因为它接受函数 fn 作为参数并执行它。

函数作为参数

将函数作为参数传递是高阶函数最常见的形式,这种被传递的函数通常称为回调函数(callback)。

基本示例

javascript
function greet(name) {
  return `Hello, ${name}!`;
}

function processUser(name, callback) {
  console.log("Processing user...");
  let message = callback(name);
  console.log(message);
}

processUser("Alice", greet);
// Processing user...
// Hello, Alice!

你也可以直接传递匿名函数:

javascript
processUser("Bob", function (name) {
  return `Welcome, ${name}!`;
});
// Processing user...
// Welcome, Bob!

// 使用箭头函数更简洁
processUser("Charlie", (name) => `Hi there, ${name}!`);
// Processing user...
// Hi there, Charlie!

自定义操作

高阶函数让你可以将"做什么"和"怎么做"分离开来:

javascript
function calculate(a, b, operation) {
  return operation(a, b);
}

// 定义不同的操作
let add = (x, y) => x + y;
let subtract = (x, y) => x - y;
let multiply = (x, y) => x * y;
let divide = (x, y) => x / y;

console.log(calculate(10, 5, add)); // 15
console.log(calculate(10, 5, subtract)); // 5
console.log(calculate(10, 5, multiply)); // 50
console.log(calculate(10, 5, divide)); // 2

这种模式让代码极具灵活性。calculate 函数不需要知道具体要执行什么操作,它只负责将两个数字传递给操作函数并返回结果。

数组遍历与转换

自定义一个通用的数组处理函数:

javascript
function processArray(array, processor) {
  let result = [];
  for (let item of array) {
    result.push(processor(item));
  }
  return result;
}

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

// 将每个数字翻倍
let doubled = processArray(numbers, (n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// 将每个数字转换为字符串
let strings = processArray(numbers, (n) => `Number: ${n}`);
console.log(strings); // ["Number: 1", "Number: 2", ...]

这个简单的 processArray 函数展示了高阶函数的威力——通过改变传入的 processor 函数,我们可以实现完全不同的数组转换。

函数作为返回值

高阶函数也可以返回函数,这在创建配置化的函数或闭包时特别有用。

函数工厂

创建一个"函数工厂",根据参数生成不同的函数:

javascript
function createMultiplier(multiplier) {
  return function (number) {
    return number * multiplier;
  };
}

let double = createMultiplier(2);
let triple = createMultiplier(3);
let quadruple = createMultiplier(4);

console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20

createMultiplier 返回一个新函数,这个新函数"记住"了 multiplier 的值。这是闭包的一个经典应用——返回的函数可以访问外部函数的参数。

创建定制的问候函数

javascript
function createGreeter(greeting) {
  return function (name) {
    return `${greeting}, ${name}!`;
  };
}

let sayHello = createGreeter("Hello");
let sayBonjour = createGreeter("Bonjour");
let sayHola = createGreeter("Hola");

console.log(sayHello("Alice")); // Hello, Alice!
console.log(sayBonjour("Marie")); // Bonjour, Marie!
console.log(sayHola("Carlos")); // Hola, Carlos!

创建验证器

javascript
function createValidator(min, max) {
  return function (value) {
    return value >= min && value <= max;
  };
}

let isValidAge = createValidator(0, 120);
let isValidPercentage = createValidator(0, 100);

console.log(isValidAge(25)); // true
console.log(isValidAge(-5)); // false
console.log(isValidAge(150)); // false

console.log(isValidPercentage(75)); // true
console.log(isValidPercentage(150)); // false

JavaScript 内置的高阶函数

JavaScript 数组提供了许多内置的高阶函数,它们是函数式编程的核心工具。

Array.prototype.map()

map() 创建一个新数组,包含对原数组每个元素调用提供的函数后的返回值:

javascript
let numbers = [1, 2, 3, 4, 5];

// 将每个数字翻倍
let doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// 转换对象数组
let users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 30 },
  { name: "Charlie", age: 35 },
];

let names = users.map((user) => user.name);
console.log(names); // ["Alice", "Bob", "Charlie"]

let userSummaries = users.map((user) => `${user.name} (${user.age} years old)`);
console.log(userSummaries);
// ["Alice (25 years old)", "Bob (30 years old)", "Charlie (35 years old)"]

map() 不会修改原数组,而是返回一个新数组。它的参数是一个函数,该函数会被应用到数组的每个元素上。

Array.prototype.filter()

filter() 创建一个新数组,包含所有通过测试函数的元素:

javascript
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 筛选出偶数
let evenNumbers = numbers.filter((n) => n % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]

// 筛选出大于5的数
let largeNumbers = numbers.filter((n) => n > 5);
console.log(largeNumbers); // [6, 7, 8, 9, 10]

// 筛选用户
let users = [
  { name: "Alice", age: 25, active: true },
  { name: "Bob", age: 17, active: false },
  { name: "Charlie", age: 35, active: true },
];

let activeAdults = users.filter((user) => user.active && user.age >= 18);
console.log(activeAdults);
// [{ name: "Alice", age: 25, active: true }, { name: "Charlie", age: 35, active: true }]

filter() 的回调函数应该返回布尔值。返回 true 的元素会被包含在新数组中,返回 false 的会被排除。

Array.prototype.reduce()

reduce() 是最强大也是最灵活的数组方法,它将数组"归约"为单个值:

javascript
let numbers = [1, 2, 3, 4, 5];

// 求和
let sum = numbers.reduce((accumulator, current) => {
  return accumulator + current;
}, 0); // 0 是初始值

console.log(sum); // 15

// 求积
let product = numbers.reduce((acc, curr) => acc * curr, 1);
console.log(product); // 120

// 找最大值
let max = numbers.reduce((max, curr) => (curr > max ? curr : max));
console.log(max); // 5

reduce() 接受两个参数:

  1. 一个回调函数,接收累加器(accumulator)和当前值(current)
  2. 初始值(可选)

更复杂的示例——统计单词出现次数:

javascript
let words = ["apple", "banana", "apple", "cherry", "banana", "apple"];

let wordCount = words.reduce((counts, word) => {
  counts[word] = (counts[word] || 0) + 1;
  return counts;
}, {});

console.log(wordCount);
// { apple: 3, banana: 2, cherry: 1 }

将多维数组扁平化:

javascript
let nestedArray = [
  [1, 2],
  [3, 4],
  [5, 6],
];

let flattened = nestedArray.reduce((flat, current) => {
  return flat.concat(current);
}, []);

console.log(flattened); // [1, 2, 3, 4, 5, 6]

Array.prototype.forEach()

forEach() 对数组的每个元素执行提供的函数:

javascript
let numbers = [1, 2, 3, 4, 5];

numbers.forEach((num, index) => {
  console.log(`Index ${index}: ${num}`);
});
// Index 0: 1
// Index 1: 2
// ...

注意: forEach() 不返回值,只是用来执行副作用(如打印、修改外部变量等)。

Array.prototype.some() 和 every()

some() 检查是否至少有一个元素通过测试:

javascript
let numbers = [1, 2, 3, 4, 5];

let hasEven = numbers.some((n) => n % 2 === 0);
console.log(hasEven); // true

let hasNegative = numbers.some((n) => n < 0);
console.log(hasNegative); // false

every() 检查是否所有元素都通过测试:

javascript
let allPositive = numbers.every((n) => n > 0);
console.log(allPositive); // true

let allEven = numbers.every((n) => n % 2 === 0);
console.log(allEven); // false

链式调用高阶函数

高阶函数的真正威力在于可以链式调用,组合多个操作:

javascript
let users = [
  { name: "Alice", age: 25, score: 85 },
  { name: "Bob", age: 17, score: 92 },
  { name: "Charlie", age: 35, score: 78 },
  { name: "David", age: 22, score: 95 },
  { name: "Eve", age: 19, score: 88 },
];

// 找出成年人中分数最高的前3名的名字
let topAdults = users
  .filter((user) => user.age >= 18) // 筛选成年人
  .sort((a, b) => b.score - a.score) // 按分数降序排序
  .slice(0, 3) // 取前3名
  .map((user) => user.name); // 提取名字

console.log(topAdults); // ["David", "Eve", "Alice"]

这种链式调用让代码读起来几乎像自然语言:"过滤成年人,按分数排序,取前三个,映射到名字"。

另一个实际例子——数据处理管道:

javascript
let transactions = [
  { id: 1, amount: 100, type: "income" },
  { id: 2, amount: 50, type: "expense" },
  { id: 3, amount: 200, type: "income" },
  { id: 4, amount: 75, type: "expense" },
  { id: 5, amount: 150, type: "income" },
];

// 计算净收入
let netIncome = transactions
  .filter((t) => t.type === "income") // 只看收入
  .map((t) => t.amount) // 提取金额
  .reduce((sum, amount) => sum + amount, 0); // 求和

console.log(netIncome); // 450

// 计算总支出
let totalExpense = transactions
  .filter((t) => t.type === "expense")
  .map((t) => t.amount)
  .reduce((sum, amount) => sum + amount, 0);

console.log(totalExpense); // 125

// 余额
console.log(`Balance: ${netIncome - totalExpense}`); // Balance: 325

实现自己的高阶函数

理解高阶函数的最好方法是自己实现一些:

实现简单的 map

javascript
function myMap(array, fn) {
  let result = [];
  for (let i = 0; i < array.length; i++) {
    result.push(fn(array[i], i, array));
  }
  return result;
}

let numbers = [1, 2, 3, 4, 5];
let doubled = myMap(numbers, (n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

实现简单的 filter

javascript
function myFilter(array, predicate) {
  let result = [];
  for (let i = 0; i < array.length; i++) {
    if (predicate(array[i], i, array)) {
      result.push(array[i]);
    }
  }
  return result;
}

let numbers = [1, 2, 3, 4, 5];
let evens = myFilter(numbers, (n) => n % 2 === 0);
console.log(evens); // [2, 4]

实现简单的 reduce

javascript
function myReduce(array, fn, initialValue) {
  let accumulator = initialValue;
  for (let i = 0; i < array.length; i++) {
    accumulator = fn(accumulator, array[i], i, array);
  }
  return accumulator;
}

let numbers = [1, 2, 3, 4, 5];
let sum = myReduce(numbers, (acc, n) => acc + n, 0);
console.log(sum); // 15

高阶函数的高级应用

函数组合

创建一个 compose 函数,将多个函数组合成一个:

javascript
function compose(...functions) {
  return function (value) {
    return functions.reduceRight((acc, fn) => fn(acc), value);
  };
}

let addOne = (x) => x + 1;
let double = (x) => x * 2;
let square = (x) => x * x;

let combined = compose(square, double, addOne);

console.log(combined(5)); // 144
// 计算过程: 5 -> addOne -> 6 -> double -> 12 -> square -> 144

部分应用(Partial Application)

创建一个函数,固定某些参数:

javascript
function partial(fn, ...fixedArgs) {
  return function (...remainingArgs) {
    return fn(...fixedArgs, ...remainingArgs);
  };
}

function greet(greeting, name, punctuation) {
  return `${greeting}, ${name}${punctuation}`;
}

let sayHello = partial(greet, "Hello");
let sayHelloToAlice = partial(greet, "Hello", "Alice");

console.log(sayHello("Bob", "!")); // Hello, Bob!
console.log(sayHelloToAlice("!!")); // Hello, Alice!!

记忆化(Memoization)

缓存函数结果以提高性能:

javascript
function memoize(fn) {
  let cache = {};
  return function (...args) {
    let key = JSON.stringify(args);
    if (key in cache) {
      console.log("Retrieved from cache");
      return cache[key];
    }
    console.log("Calculating...");
    let result = fn(...args);
    cache[key] = result;
    return result;
  };
}

// 模拟一个昂贵的计算
function expensiveCalculation(n) {
  let result = 0;
  for (let i = 0; i < n * 1000000; i++) {
    result += i;
  }
  return result;
}

let memoized = memoize(expensiveCalculation);

console.log(memoized(100)); // Calculating... (较慢)
console.log(memoized(100)); // Retrieved from cache (很快)
console.log(memoized(200)); // Calculating... (较慢)
console.log(memoized(100)); // Retrieved from cache (很快)

防抖(Debounce)

限制函数执行频率,常用于搜索输入:

javascript
function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

// 模拟搜索函数
function search(query) {
  console.log(`Searching for: ${query}`);
}

let debouncedSearch = debounce(search, 500);

// 快速连续调用
debouncedSearch("a");
debouncedSearch("ap");
debouncedSearch("app");
debouncedSearch("appl");
debouncedSearch("apple");
// 只会在最后一次调用的500ms后执行一次: "Searching for: apple"

节流(Throttle)

确保函数在指定时间内最多执行一次:

javascript
function throttle(fn, limit) {
  let inThrottle;
  return function (...args) {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// 模拟滚动事件处理
function handleScroll() {
  console.log("Scroll event processed");
}

let throttledScroll = throttle(handleScroll, 1000);

// 即使快速连续调用,也只会每1000ms执行一次
// throttledScroll();
// throttledScroll();
// throttledScroll();

常见问题与最佳实践

1. 避免在循环中创建函数

javascript
// ❌ 低效:每次循环都创建新函数
let numbers = [1, 2, 3, 4, 5];
numbers.forEach(function (n) {
  setTimeout(function () {
    console.log(n);
  }, 1000);
});

// ✅ 更好:提取函数
function logNumber(n) {
  setTimeout(function () {
    console.log(n);
  }, 1000);
}

numbers.forEach(logNumber);

2. 注意 this 绑定

使用箭头函数作为回调可以避免 this 绑定问题:

javascript
let counter = {
  count: 0,
  increment() {
    // ❌ 普通函数:this 不是 counter
    [1, 2, 3].forEach(function () {
      // this.count++; // 报错或不生效
    });

    // ✅ 箭头函数:继承外部 this
    [1, 2, 3].forEach(() => {
      this.count++;
    });
  },
};

counter.increment();
console.log(counter.count); // 3

3. 纯函数优先

尽量编写纯函数(无副作用,相同输入产生相同输出):

javascript
// ❌ 有副作用
let total = 0;
function addToTotal(n) {
  total += n; // 修改外部变量
  return total;
}

// ✅ 纯函数
function add(a, b) {
  return a + b; // 不修改外部状态
}

4. 合理使用链式调用

虽然链式调用很优雅,但过长会影响可读性:

javascript
// ❌ 过长的链
let result = data
  .filter((x) => x.active)
  .map((x) => x.value)
  .filter((x) => x > 0)
  .map((x) => x * 2)
  .filter((x) => x < 100)
  .reduce((a, b) => a + b, 0);

// ✅ 分步处理,增加可读性
let activeItems = data.filter((x) => x.active);
let values = activeItems.map((x) => x.value);
let positiveValues = values.filter((x) => x > 0);
let doubled = positiveValues.map((x) => x * 2);
let filteredValues = doubled.filter((x) => x < 100);
let result = filteredValues.reduce((a, b) => a + b, 0);

总结

高阶函数是 JavaScript 中最强大的特性之一,它们是函数式编程的基石。

关键要点:

  • 高阶函数接受函数作为参数,或返回函数作为结果
  • 将函数作为参数传递实现了策略模式,将"做什么"和"怎么做"分离
  • 返回函数可以创建函数工厂闭包
  • JavaScript 内置的 mapfilterreduce 等是最常用的高阶函数
  • 链式调用高阶函数可以构建强大的数据处理管道
  • 高阶函数可以实现函数组合、部分应用、记忆化等高级模式
  • 防抖和节流是高阶函数在性能优化中的经典应用
  • 优先使用纯函数,避免副作用
  • 注意 this 绑定,箭头函数通常是回调的好选择

掌握高阶函数会让你的代码更加灵活、可复用和富有表现力。它们是从初级开发者向高级开发者进阶的重要标志,也是理解现代 JavaScript 框架和函数式编程的必备基础。