函数声明:定义可复用的代码块
在日常生活中,当你重复做某件事情时,你可能会总结出一套固定的步骤。比如,煮咖啡的步骤总是"烧水、放咖啡粉、倒水、搅拌"。你不需要每次都重新思考这些步骤,而是直接按照这个"配方"执行。在编程中,函数声明就像是定义这样的"配方"——你一次性写好代码逻辑,然后可以在需要的时候反复调用。
什么是函数声明
函数声明是创建函数最传统、最常见的方式。它使用 function 关键字,后跟函数名、参数列表和函数体。函数声明就像是给一段代码起个名字,以便之后随时调用。
function greet() {
console.log("Hello, welcome to JavaScript!");
}
// 调用函数
greet(); // Hello, welcome to JavaScript!在这个简单的例子中,我们声明了一个名为 greet 的函数。当调用 greet() 时,函数体内的代码会被执行,输出问候语。函数声明本身不会执行任何代码,它只是定义了一个可以被调用的代码块。
函数声明的完整语法结构包含几个关键部分:
function functionName(parameter1, parameter2) {
// 函数体:这里放置要执行的代码
return result; // 可选的返回值
}function 关键字标识这是一个函数声明。紧跟其后的是函数名,它必须是有效的 JavaScript 标识符。圆括号内是参数列表,即使没有参数也必须写上空的圆括号。花括号包含函数体,这是实际执行的代码。return 语句是可选的,用于返回计算结果。
带参数的函数声明
函数的真正威力在于它可以接受输入,根据不同的输入产生不同的输出。参数就像是函数的"可变配置",让同一个函数能够处理不同的数据。
function greetPerson(name) {
console.log(`Hello, ${name}! Welcome to JavaScript!`);
}
greetPerson("Alice"); // Hello, Alice! Welcome to JavaScript!
greetPerson("Bob"); // Hello, Bob! Welcome to JavaScript!
greetPerson("Charlie"); // Hello, Charlie! Welcome to JavaScript!这里的 name 是一个参数(parameter),它在函数声明时定义。当调用函数时,传入的 "Alice"、"Bob" 等值被称为参数(argument)。在函数内部,name 会被赋值为传入的实际值。
函数可以接受多个参数,参数之间用逗号分隔:
function calculateRectangleArea(width, height) {
let area = width * height;
console.log(`Rectangle area: ${area} square units`);
return area;
}
let roomArea = calculateRectangleArea(5, 4); // Rectangle area: 20 square units
console.log(`The room area is ${roomArea} square meters`); // The room area is 20 square meters在这个例子中,函数接受两个参数 width 和 height,计算矩形面积并返回结果。注意 return 语句的使用——它不仅会返回一个值,还会立即终止函数的执行。return 之后的任何代码都不会被执行:
function checkAge(age) {
if (age < 18) {
return "Too young";
console.log("This will never execute"); // 永远不会执行
}
return "Adult";
}
console.log(checkAge(15)); // Too young
console.log(checkAge(25)); // Adult返回值的重要性
函数可以通过 return 语句返回一个值给调用者。这使得函数不仅可以执行操作,还可以产生结果供其他代码使用。
function add(a, b) {
return a + b;
}
let sum = add(5, 3);
console.log(sum); // 8
// 可以直接在表达式中使用函数返回值
let total = add(10, 20) + add(5, 15);
console.log(total); // 50如果函数没有显式的 return 语句,或者 return 后面没有值,函数会默认返回 undefined:
function logMessage(message) {
console.log(message);
// 没有 return 语句
}
let result = logMessage("Hello"); // 输出: Hello
console.log(result); // undefined一个函数可以在不同的条件分支中返回不同的值:
function getDiscount(membershipType) {
if (membershipType === "premium") {
return 0.3; // 7折
} else if (membershipType === "gold") {
return 0.2; // 8折
} else if (membershipType === "silver") {
return 0.1; // 9折
}
return 0; // 普通用户无折扣
}
console.log(getDiscount("premium")); // 0.3
console.log(getDiscount("silver")); // 0.1
console.log(getDiscount("basic")); // 0函数声明提升(Hoisting)
函数声明有一个独特而重要的特性:声明提升。这意味着你可以在函数声明之前调用函数,JavaScript 引擎会在代码执行前将函数声明"提升"到作用域的顶部。
// 在函数声明之前调用
sayHello(); // Hello, this is hoisting!
// 函数声明
function sayHello() {
console.log("Hello, this is hoisting!");
}这段代码可以正常运行,即使 sayHello() 的调用出现在函数声明之前。这是因为在代码实际执行前,JavaScript 引擎会先扫描整个作用域,将所有函数声明提升到顶部。
从内部机制来看,上面的代码在执行时等同于:
// JavaScript 引擎内部处理后的效果
function sayHello() {
console.log("Hello, this is hoisting!");
}
sayHello(); // Hello, this is hoisting!声明提升只适用于函数声明,不适用于函数表达式或箭头函数(我们会在后续章节讨论):
// ✅ 函数声明:可以提前调用
greet(); // 正常工作
function greet() {
console.log("Hello!");
}
// ❌ 函数表达式:不能提前调用
sayGoodbye(); // 报错: Cannot access 'sayGoodbye' before initialization
const sayGoodbye = function () {
console.log("Goodbye!");
};虽然声明提升让代码更灵活,但最佳实践是先声明函数再使用,这样代码更容易阅读和理解。
函数作用域
函数内部声明的变量只在函数内部可见,外部无法访问。这种封装性是函数的重要特性之一,它可以防止变量命名冲突,使代码更加模块化。
function calculateTotal() {
let price = 100; // 局部变量
let tax = price * 0.1;
let total = price + tax;
return total;
}
console.log(calculateTotal()); // 110
console.log(price); // 报错: price is not defined在这个例子中,price、tax 和 total 都是函数内部的局部变量。它们在函数外部不可访问。但是,函数内部可以访问外部作用域的变量:
let globalDiscount = 0.15; // 全局变量
function calculatePrice(basePrice) {
let discount = basePrice * globalDiscount; // 可以访问全局变量
return basePrice - discount;
}
console.log(calculatePrice(100)); // 85函数的参数也只在函数内部可见:
function processOrder(orderId, customerId) {
console.log(`Processing order ${orderId} for customer ${customerId}`);
// orderId 和 customerId 只在这个函数内部可用
}
processOrder(12345, 67890);
console.log(orderId); // 报错: orderId is not defined函数命名规范
良好的函数命名可以让代码自文档化,提高可读性和可维护性。以下是一些广为接受的命名最佳实践:
1. 使用动词开头
函数通常执行某个动作,所以用动词开头的命名更加直观:
// ✅ 好的命名
function calculateTotal() {}
function getUserInfo() {}
function validateEmail() {}
function sendNotification() {}
// ❌ 不好的命名
function total() {} // 缺少动词
function data() {} // 太模糊
function process() {} // 不够具体2. 使用驼峰命名法(camelCase)
JavaScript 的函数命名约定使用驼峰命名法,第一个单词小写,后续单词首字母大写:
// ✅ 正确的驼峰命名
function calculateMonthlyPayment() {}
function getUserProfileData() {}
function isValidCreditCard() {}
// ❌ 避免使用
function calculate_monthly_payment() {} // 下划线命名法(Python风格)
function CalculateMonthlyPayment() {} // 帕斯卡命名法(通常用于类)3. 布尔值返回函数
返回布尔值的函数通常以 is、has、can 等词开头:
function isAdult(age) {
return age >= 18;
}
function hasPermission(user, action) {
return user.permissions.includes(action);
}
function canDelete(user, post) {
return user.id === post.authorId || user.role === "admin";
}
console.log(isAdult(25)); // true
console.log(isAdult(15)); // false4. 命名要有意义
避免使用过于简短或模糊的命名:
// ❌ 不好的命名
function doStuff() {}
function fn1() {}
function temp() {}
function x() {}
// ✅ 好的命名
function processPayment() {}
function calculateShippingCost() {}
function validateUserInput() {}
function convertCurrencyToUSD() {}实际应用场景
1. 数据验证
函数声明常用于封装验证逻辑,使代码更加整洁和可复用:
function validatePassword(password) {
if (password.length < 8) {
return {
valid: false,
message: "Password must be at least 8 characters long",
};
}
if (!/[A-Z]/.test(password)) {
return {
valid: false,
message: "Password must contain at least one uppercase letter",
};
}
if (!/[0-9]/.test(password)) {
return {
valid: false,
message: "Password must contain at least one number",
};
}
return { valid: true, message: "Password is valid" };
}
let result1 = validatePassword("weak");
console.log(result1); // { valid: false, message: "Password must be at least 8 characters long" }
let result2 = validatePassword("StrongPass123");
console.log(result2); // { valid: true, message: "Password is valid" }2. 计算和业务逻辑
将计算逻辑封装在函数中,让代码更易理解和测试:
function calculateOrderTotal(items, taxRate, shippingCost) {
let subtotal = 0;
for (let item of items) {
subtotal += item.price * item.quantity;
}
let tax = subtotal * taxRate;
let total = subtotal + tax + shippingCost;
return {
subtotal: subtotal,
tax: tax,
shipping: shippingCost,
total: total,
};
}
let cart = [
{ name: "Book", price: 15, quantity: 2 },
{ name: "Pen", price: 2, quantity: 5 },
];
let orderSummary = calculateOrderTotal(cart, 0.08, 5);
console.log(orderSummary);
// {
// subtotal: 40,
// tax: 3.2,
// shipping: 5,
// total: 48.2
// }3. 数据转换
函数可以用来转换数据格式:
function formatCurrency(amount, currencyCode) {
let symbol;
switch (currencyCode) {
case "USD":
symbol = "$";
break;
case "EUR":
symbol = "€";
break;
case "GBP":
symbol = "£";
break;
default:
symbol = currencyCode;
}
return `${symbol}${amount.toFixed(2)}`;
}
console.log(formatCurrency(1234.5, "USD")); // $1234.50
console.log(formatCurrency(999.99, "EUR")); // €999.99
console.log(formatCurrency(500, "GBP")); // £500.004. 条件渲染辅助函数
在构建用户界面时,函数可以帮助决定显示什么内容:
function getGreetingMessage(hour) {
if (hour < 12) {
return "Good morning";
} else if (hour < 18) {
return "Good afternoon";
} else {
return "Good evening";
}
}
function getUserStatusBadge(user) {
if (user.isPremium) {
return "Premium Member";
} else if (user.isVerified) {
return "Verified User";
} else {
return "Regular User";
}
}
let currentHour = new Date().getHours();
console.log(getGreetingMessage(currentHour)); // 根据当前时间显示问候语
let user = { isPremium: true, isVerified: true };
console.log(getUserStatusBadge(user)); // Premium Member常见陷阱与最佳实践
1. 参数数量
避免函数有过多参数。当参数超过 3-4 个时,考虑使用对象传参:
// ❌ 参数过多
function createUser(firstName, lastName, email, age, address, phone, country) {
// ...
}
// ✅ 使用对象参数
function createUser(userData) {
let { firstName, lastName, email, age, address, phone, country } = userData;
// ...
}
createUser({
firstName: "John",
lastName: "Doe",
email: "[email protected]",
age: 30,
address: "123 Main St",
phone: "555-1234",
country: "USA",
});2. 单一职责原则
每个函数应该只做一件事,并且做好:
// ❌ 函数做了太多事情
function processUserData(user) {
// 验证用户
if (!user.email) return false;
// 保存到数据库
database.save(user);
// 发送邮件
emailService.send(user.email, "Welcome!");
// 记录日志
logger.log(`User ${user.email} registered`);
return true;
}
// ✅ 拆分成多个专注的函数
function validateUser(user) {
return Boolean(user.email);
}
function saveUser(user) {
return database.save(user);
}
function sendWelcomeEmail(email) {
return emailService.send(email, "Welcome!");
}
function logUserRegistration(email) {
logger.log(`User ${email} registered`);
}
function registerUser(user) {
if (!validateUser(user)) {
return false;
}
saveUser(user);
sendWelcomeEmail(user.email);
logUserRegistration(user.email);
return true;
}3. 避免副作用
纯函数(不产生副作用的函数)更容易测试和理解。副作用包括修改全局变量、修改传入的对象等:
// ❌ 有副作用:修改了传入的对象
function addDiscount(product) {
product.price = product.price * 0.9;
return product;
}
let item = { name: "Book", price: 100 };
addDiscount(item);
console.log(item.price); // 90 - 原始对象被修改了
// ✅ 无副作用:返回新对象
function applyDiscount(product) {
return {
...product,
price: product.price * 0.9,
};
}
let originalItem = { name: "Book", price: 100 };
let discountedItem = applyDiscount(originalItem);
console.log(originalItem.price); // 100 - 原始对象未被修改
console.log(discountedItem.price); // 90 - 新对象包含折扣价4. 提供有意义的默认行为
当参数缺失时,函数应该有合理的默认行为或明确的错误提示:
// ❌ 缺少参数检查
function divide(a, b) {
return a / b; // 如果b为0会返回Infinity
}
// ✅ 添加参数验证
function safeDivide(a, b) {
if (b === 0) {
console.error("Error: Division by zero");
return null;
}
if (typeof a !== "number" || typeof b !== "number") {
console.error("Error: Both parameters must be numbers");
return null;
}
return a / b;
}
console.log(safeDivide(10, 2)); // 5
console.log(safeDivide(10, 0)); // Error: Division by zero, null
console.log(safeDivide("10", 2)); // Error: Both parameters must be numbers, null5. 函数长度控制
一个函数不应该太长。如果函数超过 30-40 行,考虑将其拆分:
// ❌ 过长的函数
function processOrder(order) {
// 50+ lines of code doing multiple things
}
// ✅ 拆分成多个小函数
function validateOrder(order) {
// 验证逻辑
}
function calculateOrderCost(order) {
// 计算逻辑
}
function saveOrderToDatabase(order) {
// 保存逻辑
}
function sendOrderConfirmation(order) {
// 发送确认邮件
}
function processOrder(order) {
if (!validateOrder(order)) {
return false;
}
let cost = calculateOrderCost(order);
saveOrderToDatabase({ ...order, cost });
sendOrderConfirmation(order);
return true;
}总结
函数声明是 JavaScript 中最基础也是最重要的概念之一。它让我们能够将代码组织成可复用的代码块,提高代码的可读性和可维护性。
关键要点:
- 函数声明使用
function关键字定义命名函数 - 函数可以接受参数并通过
return返回值 - 函数声明会被提升,可以在声明前调用
- 函数内部的变量和参数拥有局部作用域
- 使用清晰、有意义的命名,遵循驼峰命名法
- 遵循单一职责原则,每个函数只做一件事
- 避免过多参数,考虑使用对象传参
- 尽量编写纯函数,减少副作用
- 控制函数长度,复杂逻辑应拆分成多个小函数
掌握函数声明是学习 JavaScript 的重要里程碑。在后续章节中,我们将学习函数表达式和箭头函数,它们提供了创建函数的其他方式,各有特点和适用场景。