Skip to content

函数声明:定义可复用的代码块

在日常生活中,当你重复做某件事情时,你可能会总结出一套固定的步骤。比如,煮咖啡的步骤总是"烧水、放咖啡粉、倒水、搅拌"。你不需要每次都重新思考这些步骤,而是直接按照这个"配方"执行。在编程中,函数声明就像是定义这样的"配方"——你一次性写好代码逻辑,然后可以在需要的时候反复调用。

什么是函数声明

函数声明是创建函数最传统、最常见的方式。它使用 function 关键字,后跟函数名、参数列表和函数体。函数声明就像是给一段代码起个名字,以便之后随时调用。

javascript
function greet() {
  console.log("Hello, welcome to JavaScript!");
}

// 调用函数
greet(); // Hello, welcome to JavaScript!

在这个简单的例子中,我们声明了一个名为 greet 的函数。当调用 greet() 时,函数体内的代码会被执行,输出问候语。函数声明本身不会执行任何代码,它只是定义了一个可以被调用的代码块。

函数声明的完整语法结构包含几个关键部分:

javascript
function functionName(parameter1, parameter2) {
  // 函数体:这里放置要执行的代码
  return result; // 可选的返回值
}

function 关键字标识这是一个函数声明。紧跟其后的是函数名,它必须是有效的 JavaScript 标识符。圆括号内是参数列表,即使没有参数也必须写上空的圆括号。花括号包含函数体,这是实际执行的代码。return 语句是可选的,用于返回计算结果。

带参数的函数声明

函数的真正威力在于它可以接受输入,根据不同的输入产生不同的输出。参数就像是函数的"可变配置",让同一个函数能够处理不同的数据。

javascript
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 会被赋值为传入的实际值。

函数可以接受多个参数,参数之间用逗号分隔:

javascript
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

在这个例子中,函数接受两个参数 widthheight,计算矩形面积并返回结果。注意 return 语句的使用——它不仅会返回一个值,还会立即终止函数的执行。return 之后的任何代码都不会被执行:

javascript
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 语句返回一个值给调用者。这使得函数不仅可以执行操作,还可以产生结果供其他代码使用。

javascript
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

javascript
function logMessage(message) {
  console.log(message);
  // 没有 return 语句
}

let result = logMessage("Hello"); // 输出: Hello
console.log(result); // undefined

一个函数可以在不同的条件分支中返回不同的值:

javascript
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 引擎会在代码执行前将函数声明"提升"到作用域的顶部。

javascript
// 在函数声明之前调用
sayHello(); // Hello, this is hoisting!

// 函数声明
function sayHello() {
  console.log("Hello, this is hoisting!");
}

这段代码可以正常运行,即使 sayHello() 的调用出现在函数声明之前。这是因为在代码实际执行前,JavaScript 引擎会先扫描整个作用域,将所有函数声明提升到顶部。

从内部机制来看,上面的代码在执行时等同于:

javascript
// JavaScript 引擎内部处理后的效果
function sayHello() {
  console.log("Hello, this is hoisting!");
}

sayHello(); // Hello, this is hoisting!

声明提升只适用于函数声明,不适用于函数表达式或箭头函数(我们会在后续章节讨论):

javascript
// ✅ 函数声明:可以提前调用
greet(); // 正常工作
function greet() {
  console.log("Hello!");
}

// ❌ 函数表达式:不能提前调用
sayGoodbye(); // 报错: Cannot access 'sayGoodbye' before initialization
const sayGoodbye = function () {
  console.log("Goodbye!");
};

虽然声明提升让代码更灵活,但最佳实践是先声明函数再使用,这样代码更容易阅读和理解。

函数作用域

函数内部声明的变量只在函数内部可见,外部无法访问。这种封装性是函数的重要特性之一,它可以防止变量命名冲突,使代码更加模块化。

javascript
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

在这个例子中,pricetaxtotal 都是函数内部的局部变量。它们在函数外部不可访问。但是,函数内部可以访问外部作用域的变量:

javascript
let globalDiscount = 0.15; // 全局变量

function calculatePrice(basePrice) {
  let discount = basePrice * globalDiscount; // 可以访问全局变量
  return basePrice - discount;
}

console.log(calculatePrice(100)); // 85

函数的参数也只在函数内部可见:

javascript
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. 使用动词开头

函数通常执行某个动作,所以用动词开头的命名更加直观:

javascript
// ✅ 好的命名
function calculateTotal() {}
function getUserInfo() {}
function validateEmail() {}
function sendNotification() {}

// ❌ 不好的命名
function total() {} // 缺少动词
function data() {} // 太模糊
function process() {} // 不够具体

2. 使用驼峰命名法(camelCase)

JavaScript 的函数命名约定使用驼峰命名法,第一个单词小写,后续单词首字母大写:

javascript
// ✅ 正确的驼峰命名
function calculateMonthlyPayment() {}
function getUserProfileData() {}
function isValidCreditCard() {}

// ❌ 避免使用
function calculate_monthly_payment() {} // 下划线命名法(Python风格)
function CalculateMonthlyPayment() {} // 帕斯卡命名法(通常用于类)

3. 布尔值返回函数

返回布尔值的函数通常以 ishascan 等词开头:

javascript
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)); // false

4. 命名要有意义

避免使用过于简短或模糊的命名:

javascript
// ❌ 不好的命名
function doStuff() {}
function fn1() {}
function temp() {}
function x() {}

// ✅ 好的命名
function processPayment() {}
function calculateShippingCost() {}
function validateUserInput() {}
function convertCurrencyToUSD() {}

实际应用场景

1. 数据验证

函数声明常用于封装验证逻辑,使代码更加整洁和可复用:

javascript
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. 计算和业务逻辑

将计算逻辑封装在函数中,让代码更易理解和测试:

javascript
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. 数据转换

函数可以用来转换数据格式:

javascript
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.00

4. 条件渲染辅助函数

在构建用户界面时,函数可以帮助决定显示什么内容:

javascript
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 个时,考虑使用对象传参:

javascript
// ❌ 参数过多
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. 单一职责原则

每个函数应该只做一件事,并且做好:

javascript
// ❌ 函数做了太多事情
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. 避免副作用

纯函数(不产生副作用的函数)更容易测试和理解。副作用包括修改全局变量、修改传入的对象等:

javascript
// ❌ 有副作用:修改了传入的对象
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. 提供有意义的默认行为

当参数缺失时,函数应该有合理的默认行为或明确的错误提示:

javascript
// ❌ 缺少参数检查
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, null

5. 函数长度控制

一个函数不应该太长。如果函数超过 30-40 行,考虑将其拆分:

javascript
// ❌ 过长的函数
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 的重要里程碑。在后续章节中,我们将学习函数表达式和箭头函数,它们提供了创建函数的其他方式,各有特点和适用场景。