Skip to content

默认参数:让函数参数拥有默认值

当你在咖啡店点咖啡时,服务员会问:"要加糖吗?"如果你不回答,有的咖啡店会默认不加糖,有的会默认加一份糖。这个"默认行为"让点单流程更顺畅——你不必每次都明确说明每个细节。在 JavaScript 中,默认参数就提供了类似的便利,让函数在某些参数未提供时能够自动使用预设的默认值。

什么是默认参数

默认参数(Default Parameters)是 ES6 引入的特性,允许我们在函数声明时为参数指定默认值。如果调用函数时没有提供该参数,或者提供的值是 undefined,函数会使用默认值:

javascript
function greet(name = "Guest") {
  console.log(`Hello, ${name}!`);
}

greet("Alice"); // Hello, Alice!
greet(); // Hello, Guest!

在这个简单的例子中,如果调用 greet() 时没有传入 name 参数,它会使用默认值 "Guest"。这比传统的参数检查方式更简洁清晰。

ES6 之前的做法

在默认参数语法出现之前,我们需要手动检查参数并设置默认值:

javascript
// ES5 及之前的常见做法
function greetOldWay(name) {
  // 方法1:使用 || 运算符
  name = name || "Guest";
  console.log("Hello, " + name + "!");
}

// 或者
function greetMoreExplicit(name) {
  // 方法2:显式检查 undefined
  if (name === undefined) {
    name = "Guest";
  }
  console.log("Hello, " + name + "!");
}

greetOldWay(); // Hello, Guest!
greetOldWay("Alice"); // Hello, Alice!

这种方法虽然可行,但有一些问题。使用 || 运算符时,如果传入的值是假值(如 0""false),也会被替换为默认值:

javascript
function setCount(count) {
  count = count || 10; // 问题:0 也会被当作假值
  console.log(`Count: ${count}`);
}

setCount(5); // Count: 5
setCount(0); // Count: 10 (本想设置为0,但被替换成了默认值)
setCount(); // Count: 10

而 ES6 的默认参数只在参数为 undefined 时生效,不会有这个问题:

javascript
function setCount(count = 10) {
  console.log(`Count: ${count}`);
}

setCount(5); // Count: 5
setCount(0); // Count: 0 (正确处理了0)
setCount(); // Count: 10
setCount(undefined); // Count: 10

默认参数的工作原理

默认参数只在两种情况下生效:

  1. 未传递该参数
  2. 显式传递 undefined

传递其他假值(如 null0false"")都不会触发默认值:

javascript
function test(value = "default") {
  console.log(value);
}

test(); // "default" - 未传递参数
test(undefined); // "default" - 显式传递 undefined
test(null); // null - null 不触发默认值
test(0); // 0
test(false); // false
test(""); // "" - 空字符串不触发默认值

这是一个重要的区别。undefined 表示"缺少值",而 null 表示"有值,但值是 null"。默认参数的设计遵循了这个语义:

javascript
function createUser(name, age = 18) {
  return {
    name: name,
    age: age,
  };
}

console.log(createUser("Alice"));
// { name: "Alice", age: 18 }

console.log(createUser("Bob", undefined));
// { name: "Bob", age: 18 }

console.log(createUser("Charlie", null));
// { name: "Charlie", age: null } - null 不触发默认值

多个默认参数

一个函数可以有多个默认参数,它们可以按任意组合使用:

javascript
function bookFlight(destination, date = "2025-12-31", class = "economy", meal = "standard") {
  console.log(`Booking flight to ${destination}`);
  console.log(`Date: ${date}`);
  console.log(`Class: ${class}`);
  console.log(`Meal: ${meal}`);
}

bookFlight("Tokyo");
// Booking flight to Tokyo
// Date: 2025-12-31
// Class: economy
// Meal: standard

bookFlight("Paris", "2025-06-15");
// Booking flight to Paris
// Date: 2025-06-15
// Class: economy
// Meal: standard

bookFlight("London", "2025-08-01", "business");
// Booking flight to London
// Date: 2025-08-01
// Class: business
// Meal: standard

跳过中间参数

如果你想使用某个参数的默认值,但又想设置它之后的参数,可以显式传递 undefined:

javascript
function configure(host, port = 8080, protocol = "http", timeout = 5000) {
  console.log(`${protocol}://${host}:${port} (timeout: ${timeout}ms)`);
}

// 想使用 port 的默认值,但自定义 protocol
configure("localhost", undefined, "https");
// https://localhost:8080 (timeout: 5000ms)

// 想使用 port 和 protocol 的默认值,但自定义 timeout
configure("localhost", undefined, undefined, 10000);
// http://localhost:8080 (timeout: 10000ms)

虽然这样可行,但多个 undefined 会影响可读性。在这种情况下,使用对象参数可能更好:

javascript
function configure({ host, port = 8080, protocol = "http", timeout = 5000 }) {
  console.log(`${protocol}://${host}:${port} (timeout: ${timeout}ms)`);
}

configure({
  host: "localhost",
  protocol: "https",
});
// https://localhost:8080 (timeout: 5000ms)

configure({
  host: "localhost",
  timeout: 10000,
});
// http://localhost:8080 (timeout: 10000ms)

默认参数表达式

默认参数的值不仅可以是简单的字面量,还可以是任何有效的 JavaScript 表达式:

1. 函数调用作为默认值

javascript
function getDefaultDate() {
  return new Date().toISOString().split("T")[0]; // 返回今天的日期
}

function createEvent(name, date = getDefaultDate()) {
  console.log(`Event: ${name} on ${date}`);
}

createEvent("Conference");
// Event: Conference on 2025-12-04

createEvent("Workshop", "2025-12-15");
// Event: Workshop on 2025-12-15

重要:默认参数表达式是在调用时计算的,而不是定义时:

javascript
let counter = 0;

function increment(value = ++counter) {
  console.log(value);
}

increment(); // 1 (counter 变成 1)
increment(); // 2 (counter 变成 2)
increment(10); // 10 (不使用默认值,counter 保持为 2)
increment(); // 3 (counter 变成 3)

2. 引用其他参数

默认参数可以引用前面的参数:

javascript
function createUser(
  firstName,
  lastName,
  fullName = `${firstName} ${lastName}`
) {
  return {
    firstName: firstName,
    lastName: lastName,
    fullName: fullName,
  };
}

console.log(createUser("John", "Doe"));
// { firstName: "John", lastName: "Doe", fullName: "John Doe" }

console.log(createUser("Jane", "Smith", "Dr. Jane Smith"));
// { firstName: "Jane", lastName: "Smith", fullName: "Dr. Jane Smith" }

但是,你不能引用后面的参数,因为它们还没有被初始化:

javascript
// ❌ 错误:不能引用后面的参数
// function invalid(a = b, b = 2) {} // ReferenceError: Cannot access 'b' before initialization

// ✅ 正确:可以引用前面的参数
function valid(a = 1, b = a * 2) {
  console.log(a, b);
}

valid(); // 1 2
valid(5); // 5 10

3. 复杂表达式

默认参数可以是任何表达式,包括三元运算符、逻辑运算等:

javascript
function calculatePrice(basePrice, discount = basePrice > 100 ? 0.1 : 0.05) {
  let finalPrice = basePrice * (1 - discount);
  console.log(
    `Original: $${basePrice}, Discount: ${
      discount * 100
    }%, Final: $${finalPrice}`
  );
}

calculatePrice(150);
// Original: $150, Discount: 10%, Final: $135

calculatePrice(80);
// Original: $80, Discount: 5%, Final: $76

4. 对象和数组字面量

javascript
function createConfig(options = { debug: false, retries: 3 }) {
  console.log(`Debug mode: ${options.debug}`);
  console.log(`Max retries: ${options.retries}`);
}

createConfig();
// Debug mode: false
// Max retries: 3

createConfig({ debug: true, retries: 5 });
// Debug mode: true
// Max retries: 5

与解构结合使用

默认参数与解构赋值结合使用时非常强大:

对象解构参数

javascript
function createUser({ name, age = 18, role = "user", active = true } = {}) {
  // 注意末尾的 = {} 提供了整个参数的默认值
  return { name, age, role, active };
}

console.log(createUser({ name: "Alice" }));
// { name: "Alice", age: 18, role: "user", active: true }

console.log(createUser({ name: "Bob", age: 25, role: "admin" }));
// { name: "Bob", age: 25, role: "admin", active: true }

console.log(createUser());
// { name: undefined, age: 18, role: "user", active: true }

注意 = {} 在函数参数末尾的作用——它为整个参数对象提供了默认值。没有它,如果调用 createUser() 而不传参数,会尝试解构 undefined,导致错误:

javascript
// ❌ 没有默认空对象
function createUserBad({ name, age = 18 }) {
  return { name, age };
}

// createUserBad(); // TypeError: Cannot destructure property 'name' of 'undefined'

// ✅ 有默认空对象
function createUserGood({ name, age = 18 } = {}) {
  return { name, age };
}

createUserGood(); // { name: undefined, age: 18 }

数组解构参数

javascript
function processCoordinates([x = 0, y = 0, z = 0] = []) {
  console.log(`Position: (${x}, ${y}, ${z})`);
}

processCoordinates([10, 20, 30]);
// Position: (10, 20, 30)

processCoordinates([5, 10]);
// Position: (5, 10, 0)

processCoordinates();
// Position: (0, 0, 0)

与剩余参数结合

默认参数可以和剩余参数一起使用,但剩余参数不能有默认值(因为它总是一个数组):

javascript
function logMessages(level = "info", ...messages) {
  console.log(`[${level.toUpperCase()}]`);
  messages.forEach((msg) => console.log(`  ${msg}`));
}

logMessages(); // [INFO] (没有消息)

logMessages("error", "Connection failed", "Retrying...");
// [ERROR]
//   Connection failed
//   Retrying...

logMessages(undefined, "Using default level");
// [INFO]
//   Using default level

实际应用场景

1. API 请求配置

javascript
function fetchData(
  url,
  { method = "GET", headers = {}, timeout = 5000, retries = 3 } = {}
) {
  console.log(`Fetching ${url}`);
  console.log(`Method: ${method}`);
  console.log(`Timeout: ${timeout}ms`);
  console.log(`Max retries: ${retries}`);
  console.log(`Headers: ${JSON.stringify(headers)}`);
  // 实际的 fetch 逻辑...
}

// 使用默认值
fetchData("/api/users");

// 覆盖部分默认值
fetchData("/api/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
});

2. 数据格式化

javascript
function formatCurrency(
  amount,
  currency = "USD",
  locale = "en-US",
  options = {}
) {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currency,
    ...options,
  }).format(amount);
}

console.log(formatCurrency(1234.56));
// $1,234.56

console.log(formatCurrency(1234.56, "EUR", "de-DE"));
// 1.234,56 €

console.log(formatCurrency(1234.56, "JPY", "ja-JP"));
// ¥1,235

3. 创建 HTML 元素

javascript
function createElement(tag, content = "", attributes = {}, children = []) {
  let attrs = Object.entries(attributes)
    .map(([key, value]) => `${key}="${value}"`)
    .join(" ");

  let attrsString = attrs ? " " + attrs : "";
  let childrenHTML = children.join("");

  return `<${tag}${attrsString}>${content}${childrenHTML}</${tag}>`;
}

console.log(createElement("h1", "Welcome"));
// <h1>Welcome</h1>

console.log(createElement("div", "", { class: "container", id: "main" }));
// <div class="container" id="main"></div>

console.log(
  createElement("ul", "", {}, ["<li>Item 1</li>", "<li>Item 2</li>"])
);
// <ul><li>Item 1</li><li>Item 2</li></ul>

4. 分页和过滤

javascript
function getUsers({
  page = 1,
  limit = 10,
  sortBy = "createdAt",
  order = "desc",
  filters = {},
} = {}) {
  console.log(`Fetching page ${page} (${limit} items per page)`);
  console.log(`Sort by: ${sortBy} (${order})`);
  console.log(`Filters: ${JSON.stringify(filters)}`);
  // 实际的数据库查询...
}

getUsers(); // 使用所有默认值
getUsers({ page: 2 }); // 只改变页码
getUsers({ limit: 20, sortBy: "name", order: "asc" });
getUsers({ filters: { role: "admin", active: true } });

常见陷阱与最佳实践

1. 默认值是引用类型时要小心

如果默认值是对象或数组,它会在每次调用时被重用(除非重新计算):

javascript
// ⚠️ 潜在问题:共享同一个对象
let defaultOptions = { debug: false };

function configure(options = defaultOptions) {
  options.debug = true; // 修改了默认对象
  console.log(options);
}

configure(); // { debug: true }
defaultOptions.debug; // true - 默认对象被修改了!

// ✅ 更好:每次创建新对象
function configureSafe(options = { debug: false }) {
  options.debug = true;
  console.log(options);
}

// 或者使用解构创建新对象
function configureBest(options = {}) {
  let config = { debug: false, ...options };
  config.debug = true;
  console.log(config);
}

2. 默认值的求值顺序

默认值从左到右求值,前面的参数可以被后面的引用:

javascript
function buildQuery(table, where = {}, limit = 10, offset = limit * 0) {
  console.log(`SELECT * FROM ${table}`);
  if (Object.keys(where).length > 0) {
    console.log(`WHERE ${JSON.stringify(where)}`);
  }
  console.log(`LIMIT ${limit} OFFSET ${offset}`);
}

buildQuery("users");
// SELECT * FROM users
// LIMIT 10 OFFSET 0

buildQuery("posts", { published: true }, 20);
// SELECT * FROM posts
// WHERE {"published":true}
// LIMIT 20 OFFSET 0

3. 避免复杂的默认值表达式

虽然默认值可以是复杂表达式,但过于复杂会降低可读性:

javascript
// ❌ 过于复杂
function process(
  data,
  transform = (data) =>
    data
      .filter((x) => x > 0)
      .map((x) => x * 2)
      .reduce((a, b) => a + b, 0)
) {
  return transform(data);
}

// ✅ 更清晰:将复杂逻辑提取出来
function defaultTransform(data) {
  return data
    .filter((x) => x > 0)
    .map((x) => x * 2)
    .reduce((a, b) => a + b, 0);
}

function processBetter(data, transform = defaultTransform) {
  return transform(data);
}

4. null vs undefined

记住 null 不会触发默认值:

javascript
function greet(name = "Guest") {
  console.log(`Hello, ${name}!`);
}

greet(undefined); // Hello, Guest!
greet(null); // Hello, null!

// 如果需要将 null 也当作缺失值,手动检查
function greetBetter(name = "Guest") {
  name = name === null ? "Guest" : name;
  console.log(`Hello, ${name}!`);
}

greetBetter(null); // Hello, Guest!

5. 必需参数在前,可选参数在后

将必需参数放在前面,有默认值的可选参数放在后面:

javascript
// ✅ 好:必需参数在前
function createArticle(
  title,
  content,
  author = "Anonymous",
  published = false
) {
  return { title, content, author, published };
}

// ❌ 不好:必需参数在后
function createArticleBad(
  author = "Anonymous",
  published = false,
  title,
  content
) {
  // 使用时必须传递前面的参数,即使想用默认值
  return { title, content, author, published };
}

createArticle("My Post", "Content here");
// { title: "My Post", content: "Content here", author: "Anonymous", published: false }

// createArticleBad 使用起来很别扭
createArticleBad(undefined, undefined, "My Post", "Content here");

总结

默认参数是 ES6 引入的重要特性,它让函数参数处理更加优雅和健壮。

关键要点:

  • 默认参数在参数为 undefined 时生效,不包括 null 等其他假值
  • 默认值可以是字面量、表达式、函数调用等任何有效的 JavaScript 表达式
  • 默认值在每次调用时计算,而非定义时
  • 可以引用前面的参数,但不能引用后面的参数
  • 与解构结合使用非常强大,但要注意提供默认空对象/数组
  • 可以与剩余参数结合,但剩余参数本身不能有默认值
  • 避免默认值是可变的引用类型,以防意外修改
  • 保持默认值表达式简单,复杂逻辑应提取到函数外
  • 将必需参数放在前,可选参数放在后
  • 使用对象参数模式可以更灵活地处理多个可选参数

默认参数显著提高了代码的可读性和健壮性,是现代 JavaScript 开发的标准实践。配合解构和剩余参数,你可以创建非常灵活和优雅的函数接口。