函数表达式:灵活的函数定义方式
如果函数声明像是在工具箱里放置一个带标签的工具,那么函数表达式就像是在需要时临时制作一个工具并立即使用。两种方式都能完成工作,但使用场景和特性有所不同。函数表达式为 JavaScript 提供了更大的灵活性,让函数可以像普通值一样被赋值、传递和使用。
什么是函数表达式
函数表达式是将函数作为值赋给变量或常量的一种方式。与函数声明不同,函数表达式是表达式的一部分,而不是独立的语句。
const greet = function () {
console.log("Hello from function expression!");
};
greet(); // Hello from function expression!在这个例子中,我们创建了一个函数并将其赋值给常量 greet。这个函数本身没有名字(匿名函数),但我们通过变量名来引用它。调用方式和函数声明完全相同,都是使用变量名加圆括号。
函数表达式和函数声明的关键区别在于:函数表达式不会被提升。这意味着你必须先定义函数表达式,然后才能使用它:
// ❌ 这会报错
sayHello(); // Error: Cannot access 'sayHello' before initialization
const sayHello = function () {
console.log("Hello!");
};
// ✅ 先定义后使用
const sayGoodbye = function () {
console.log("Goodbye!");
};
sayGoodbye(); // Goodbye! - 正常工作匿名函数表达式
最常见的函数表达式形式是匿名函数——函数本身没有名字,只通过变量来引用:
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):
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 来调用。命名函数表达式的优势在于:
- 递归调用:函数可以在内部通过自己的名字调用自己,即使外部变量被重新赋值也不受影响
- 调试友好:错误栈中会显示函数的实际名字,而不是 "anonymous"
- 代码可读性:函数名可以描述函数的用途,提高代码可读性
让我们看一个更实际的例子:
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)是一种定义后立即执行的函数。它的语法特征是用圆括号包裹函数定义,然后在后面加上调用括号:
(function () {
console.log("This function runs immediately!");
})();
// This function runs immediately!IIFE 的第一对圆括号将函数声明转换为表达式,第二对圆括号立即调用这个函数。这种模式在 ES6 之前非常流行,用于创建私有作用域:
(function () {
let privateVariable = "I'm private";
console.log(privateVariable); // I'm private
})();
console.log(privateVariable); // 错误: privateVariable is not definedIIFE 也可以接受参数和返回值:
let result = (function (a, b) {
return a + b;
})(5, 3);
console.log(result); // 8一个实际应用场景是避免全局命名空间污染:
// 创建一个独立的模块作用域
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 中,我们有了 let、const 和模块系统,IIFE 的使用有所减少,但理解它依然很重要,因为你会在许多现有代码库中看到这种模式。
函数作为值
函数表达式的真正威力在于函数可以像其他值一样被使用——可以赋值给变量、作为参数传递、作为返回值返回、存储在数组或对象中。
存储在数据结构中
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]作为参数传递(回调函数)
函数表达式最常见的用途之一是作为回调函数传递给其他函数:
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回调函数在异步编程中也极其重要:
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);
});作为返回值
函数可以返回另一个函数,这种模式称为"高阶函数",是函数式编程的核心概念之一:
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返回的函数可以"记住"外部函数的参数,这就形成了闭包:
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. 数据封装和私有变量
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. 函数工厂
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. 事件处理器
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. 部分应用和柯里化
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 | 回调、闭包、高阶函数 |
选择使用哪种方式取决于具体场景:
// ✅ 函数声明:适合顶层、可复用的工具函数
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 绑定可能会让人困惑:
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 Charlie2. 避免在循环中创建函数
在循环中创建函数表达式要特别注意闭包的行为:
// ❌ 常见错误
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](); // 23. 内存管理
闭包会保持对外部变量的引用,这可能导致内存泄漏:
// ❌ 可能的内存泄漏
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. 命名函数表达式用于调试
对于复杂的函数表达式,使用命名形式可以提高可调试性:
// ❌ 难以调试
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 引入的更简洁的函数表达式语法。