Skip to content

函数表达式:灵活的函数定义方式

如果函数声明像是在工具箱里放置一个带标签的工具,那么函数表达式就像是在需要时临时制作一个工具并立即使用。两种方式都能完成工作,但使用场景和特性有所不同。函数表达式为 JavaScript 提供了更大的灵活性,让函数可以像普通值一样被赋值、传递和使用。

什么是函数表达式

函数表达式是将函数作为值赋给变量或常量的一种方式。与函数声明不同,函数表达式是表达式的一部分,而不是独立的语句。

javascript
const greet = function () {
  console.log("Hello from function expression!");
};

greet(); // Hello from function expression!

在这个例子中,我们创建了一个函数并将其赋值给常量 greet。这个函数本身没有名字(匿名函数),但我们通过变量名来引用它。调用方式和函数声明完全相同,都是使用变量名加圆括号。

函数表达式和函数声明的关键区别在于:函数表达式不会被提升。这意味着你必须先定义函数表达式,然后才能使用它:

javascript
// ❌ 这会报错
sayHello(); // Error: Cannot access 'sayHello' before initialization

const sayHello = function () {
  console.log("Hello!");
};

// ✅ 先定义后使用
const sayGoodbye = function () {
  console.log("Goodbye!");
};

sayGoodbye(); // Goodbye! - 正常工作

匿名函数表达式

最常见的函数表达式形式是匿名函数——函数本身没有名字,只通过变量来引用:

javascript
const add = function (a, b) {
  return a + b;
};

const multiply = function (x, y) {
  return x * y;
};

console.log(add(5, 3)); // 8
console.log(multiply(4, 7)); // 28

匿名函数表达式简洁明了,特别适合用作简短的辅助函数或回调函数。但在调试时,匿名函数可能会让错误栈追踪变得困难,因为它们在调试器中显示为 "anonymous"。

命名函数表达式

函数表达式也可以有自己的名字,这被称为命名函数表达式(Named Function Expression, NFE):

javascript
const factorial = function calculateFactorial(n) {
  if (n <= 1) {
    return 1;
  }
  return n * calculateFactorial(n - 1); // 在函数内部使用自己的名字
};

console.log(factorial(5)); // 120
console.log(calculateFactorial(5)); // 错误: calculateFactorial is not defined

注意这里的微妙之处:函数名 calculateFactorial 只在函数内部可见,外部必须通过变量名 factorial 来调用。命名函数表达式的优势在于:

  1. 递归调用:函数可以在内部通过自己的名字调用自己,即使外部变量被重新赋值也不受影响
  2. 调试友好:错误栈中会显示函数的实际名字,而不是 "anonymous"
  3. 代码可读性:函数名可以描述函数的用途,提高代码可读性

让我们看一个更实际的例子:

javascript
const timer = function countdown(seconds) {
  console.log(`${seconds} seconds remaining`);

  if (seconds > 0) {
    setTimeout(function () {
      countdown(seconds - 1); // 递归调用
    }, 1000);
  } else {
    console.log("Time's up!");
  }
};

timer(3);
// 3 seconds remaining
// 2 seconds remaining
// 1 seconds remaining
// 0 seconds remaining
// Time's up!

立即执行函数表达式(IIFE)

立即执行函数表达式(Immediately Invoked Function Expression,简称 IIFE)是一种定义后立即执行的函数。它的语法特征是用圆括号包裹函数定义,然后在后面加上调用括号:

javascript
(function () {
  console.log("This function runs immediately!");
})();
// This function runs immediately!

IIFE 的第一对圆括号将函数声明转换为表达式,第二对圆括号立即调用这个函数。这种模式在 ES6 之前非常流行,用于创建私有作用域:

javascript
(function () {
  let privateVariable = "I'm private";
  console.log(privateVariable); // I'm private
})();

console.log(privateVariable); // 错误: privateVariable is not defined

IIFE 也可以接受参数和返回值:

javascript
let result = (function (a, b) {
  return a + b;
})(5, 3);

console.log(result); // 8

一个实际应用场景是避免全局命名空间污染:

javascript
// 创建一个独立的模块作用域
const calculator = (function () {
  // 私有变量和函数
  let lastResult = 0;

  function log(operation, result) {
    console.log(`${operation} = ${result}`);
  }

  // 返回公共接口
  return {
    add: function (a, b) {
      lastResult = a + b;
      log(`${a} + ${b}`, lastResult);
      return lastResult;
    },
    subtract: function (a, b) {
      lastResult = a - b;
      log(`${a} - ${b}`, lastResult);
      return lastResult;
    },
    getLastResult: function () {
      return lastResult;
    },
  };
})();

calculator.add(10, 5); // 10 + 5 = 15
calculator.subtract(20, 8); // 20 - 8 = 12
console.log(calculator.getLastResult()); // 12
console.log(calculator.lastResult); // undefined - 私有变量无法直接访问

虽然在现代 JavaScript 中,我们有了 letconst 和模块系统,IIFE 的使用有所减少,但理解它依然很重要,因为你会在许多现有代码库中看到这种模式。

函数作为值

函数表达式的真正威力在于函数可以像其他值一样被使用——可以赋值给变量、作为参数传递、作为返回值返回、存储在数组或对象中。

存储在数据结构中

javascript
const operations = {
  add: function (a, b) {
    return a + b;
  },
  subtract: function (a, b) {
    return a - b;
  },
  multiply: function (a, b) {
    return a * b;
  },
  divide: function (a, b) {
    return b !== 0 ? a / b : "Cannot divide by zero";
  },
};

console.log(operations.add(10, 5)); // 15
console.log(operations.multiply(4, 7)); // 28
console.log(operations.divide(20, 4)); // 5

// 函数数组
const filters = [
  function (n) {
    return n > 0;
  }, // 正数过滤器
  function (n) {
    return n % 2 === 0;
  }, // 偶数过滤器
  function (n) {
    return n < 100;
  }, // 小于100过滤器
];

let numbers = [-5, 2, 8, 15, 42, 101, 150];
let result = numbers.filter(filters[0]).filter(filters[1]).filter(filters[2]);
console.log(result); // [2, 8, 42]

作为参数传递(回调函数)

函数表达式最常见的用途之一是作为回调函数传递给其他函数:

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

// 使用匿名函数表达式作为回调
const doubled = numbers.map(function (num) {
  return num * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]

// 多个回调示例
const evenNumbers = numbers.filter(function (num) {
  return num % 2 === 0;
});
console.log(evenNumbers); // [2, 4]

const sum = numbers.reduce(function (total, num) {
  return total + num;
}, 0);
console.log(sum); // 15

回调函数在异步编程中也极其重要:

javascript
function fetchUserData(userId, callback) {
  console.log(`Fetching user data for user ${userId}...`);

  // 模拟异步操作
  setTimeout(function () {
    const userData = {
      id: userId,
      name: "Sarah Johnson",
      email: "[email protected]",
    };
    callback(userData); // 调用回调函数
  }, 1000);
}

// 传入回调函数
fetchUserData(123, function (user) {
  console.log("User data received:");
  console.log(user);
});

作为返回值

函数可以返回另一个函数,这种模式称为"高阶函数",是函数式编程的核心概念之一:

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

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

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

返回的函数可以"记住"外部函数的参数,这就形成了闭包:

javascript
function createCounter() {
  let count = 0; // 私有变量

  return function () {
    count++; // 访问外部变量
    return count;
  };
}

const counter1 = createCounter();
const counter2 = createCounter();

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter1()); // 3

console.log(counter2()); // 1 - 独立的计数器
console.log(counter2()); // 2

闭包的实际应用

函数表达式与闭包的结合创造了强大的模式。闭包允许函数"记住"其创建时的环境,即使在外部函数执行完毕后仍然可以访问外部变量。

1. 数据封装和私有变量

javascript
function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有变量
  let transactionHistory = []; // 私有变量

  return {
    deposit: function (amount) {
      if (amount > 0) {
        balance += amount;
        transactionHistory.push({ type: "deposit", amount, balance });
        console.log(`Deposited $${amount}. New balance: $${balance}`);
        return true;
      }
      return false;
    },

    withdraw: function (amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        transactionHistory.push({ type: "withdraw", amount, balance });
        console.log(`Withdrew $${amount}. New balance: $${balance}`);
        return true;
      }
      console.log("Insufficient funds or invalid amount");
      return false;
    },

    getBalance: function () {
      return balance;
    },

    getHistory: function () {
      return [...transactionHistory]; // 返回副本,防止外部修改
    },
  };
}

const myAccount = createBankAccount(1000);
myAccount.deposit(500); // Deposited $500. New balance: $1500
myAccount.withdraw(200); // Withdrew $200. New balance: $1300
console.log(myAccount.getBalance()); // 1300
console.log(myAccount.balance); // undefined - 无法直接访问私有变量

2. 函数工厂

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

const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
const sayGoodMorning = createGreeter("Good morning");

sayHello("Alice"); // Hello, Alice!
sayHi("Bob"); // Hi, Bob!
sayGoodMorning("Charlie"); // Good morning, Charlie!

3. 事件处理器

javascript
function setupButtonHandlers() {
  const buttons = ["home", "about", "contact"];

  for (let i = 0; i < buttons.length; i++) {
    // 使用 IIFE 创建闭包
    (function (index) {
      const buttonName = buttons[index];

      // 模拟添加事件监听器
      console.log(`Setting up handler for ${buttonName} button`);

      // 事件处理函数
      const handler = function () {
        console.log(`${buttonName} button clicked (index: ${index})`);
      };

      // 模拟点击
      setTimeout(handler, (index + 1) * 1000);
    })(i);
  }
}

setupButtonHandlers();
// Setting up handler for home button
// Setting up handler for about button
// Setting up handler for contact button
// (1秒后) home button clicked (index: 0)
// (2秒后) about button clicked (index: 1)
// (3秒后) contact button clicked (index: 2)

4. 部分应用和柯里化

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

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

const sayHelloTo = partial(greet, "Hello");
const sayHelloWorldWith = partial(greet, "Hello", "World");

console.log(sayHelloTo("Alice", "!")); // Hello, Alice!
console.log(sayHelloTo("Bob", ".")); // Hello, Bob.
console.log(sayHelloWorldWith("!")); // Hello, World!

函数表达式 vs 函数声明

让我们总结一下函数表达式和函数声明的主要区别:

特性函数声明函数表达式
语法function name() {}const name = function() {}
提升会被提升,可以在声明前调用不会提升,必须先定义
命名必须有名字可以是匿名的
作为值使用不能直接赋值或传递可以像其他值一样使用
使用场景定义顶层函数,公共 API回调、闭包、高阶函数

选择使用哪种方式取决于具体场景:

javascript
// ✅ 函数声明:适合顶层、可复用的工具函数
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// ✅ 函数表达式:适合作为回调
const items = [{ price: 10 }, { price: 20 }, { price: 30 }];
items.forEach(function (item) {
  console.log(item.price);
});

// ✅ 函数表达式:适合创建闭包
const counter = (function () {
  let count = 0;
  return {
    increment: function () {
      return ++count;
    },
  };
})();

常见陷阱与最佳实践

1. 注意 this 绑定

函数表达式中的 this 绑定可能会让人困惑:

javascript
const person = {
  name: "Alice",
  greet: function () {
    console.log(`Hello, I'm ${this.name}`);
  },
  greetLater: function () {
    setTimeout(function () {
      console.log(`Hello, I'm ${this.name}`); // this 指向不同的对象
    }, 1000);
  },
};

person.greet(); // Hello, I'm Alice
person.greetLater(); // Hello, I'm undefined

// 解决方案1: 使用变量保存 this
const person2 = {
  name: "Bob",
  greetLater: function () {
    const self = this; // 保存 this 引用
    setTimeout(function () {
      console.log(`Hello, I'm ${self.name}`);
    }, 1000);
  },
};

person2.greetLater(); // Hello, I'm Bob

// 解决方案2: 使用箭头函数(后续章节会详细讲解)
const person3 = {
  name: "Charlie",
  greetLater: function () {
    setTimeout(() => {
      console.log(`Hello, I'm ${this.name}`);
    }, 1000);
  },
};

person3.greetLater(); // Hello, I'm Charlie

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

在循环中创建函数表达式要特别注意闭包的行为:

javascript
// ❌ 常见错误
const functions = [];

for (var i = 0; i < 3; i++) {
  functions.push(function () {
    console.log(i);
  });
}

functions[0](); // 3
functions[1](); // 3
functions[2](); // 3 - 都打印 3!

// ✅ 解决方案1: 使用 let
const functions2 = [];

for (let i = 0; i < 3; i++) {
  functions2.push(function () {
    console.log(i);
  });
}

functions2[0](); // 0
functions2[1](); // 1
functions2[2](); // 2

// ✅ 解决方案2: 使用 IIFE
const functions3 = [];

for (var i = 0; i < 3; i++) {
  (function (index) {
    functions3.push(function () {
      console.log(index);
    });
  })(i);
}

functions3[0](); // 0
functions3[1](); // 1
functions3[2](); // 2

3. 内存管理

闭包会保持对外部变量的引用,这可能导致内存泄漏:

javascript
// ❌ 可能的内存泄漏
function createHeavyObject() {
  const largeArray = new Array(1000000).fill("data"); // 大数据

  return function () {
    // 即使不使用 largeArray,它也会一直存在于内存中
    console.log("Using closure");
  };
}

// ✅ 只保留需要的数据
function createOptimizedObject() {
  const largeArray = new Array(1000000).fill("data");
  const summary = largeArray.length; // 只保留摘要信息

  return function () {
    console.log(`Array had ${summary} elements`);
    // largeArray 可以被垃圾回收
  };
}

4. 命名函数表达式用于调试

对于复杂的函数表达式,使用命名形式可以提高可调试性:

javascript
// ❌ 难以调试
const processData = function (data) {
  // 50+ lines of code
  throw new Error("Something went wrong");
};

// ✅ 更好的调试体验
const processData = function processUserData(data) {
  // 50+ lines of code
  throw new Error("Something went wrong");
  // 错误栈会显示 "processUserData" 而不是 "anonymous"
};

总结

函数表达式为 JavaScript 提供了强大的函数处理能力,让函数成为"一等公民"。理解函数表达式是掌握 JavaScript 高级特性的关键。

关键要点:

  • 函数表达式将函数赋值给变量,不会被提升
  • 匿名函数表达式简洁,命名函数表达式便于调试和递归
  • IIFE 用于创建独立作用域,避免命名冲突
  • 函数可以作为值传递、返回和存储
  • 回调函数是函数表达式最常见的应用
  • 闭包允许函数记住其创建环境
  • 注意 this 绑定和循环中的闭包陷阱
  • 合理使用闭包,避免内存泄漏

函数表达式与函数声明各有用途,灵活运用两者可以写出更优雅、更强大的代码。在下一章中,我们将学习箭头函数,它是 ES6 引入的更简洁的函数表达式语法。