默认参数:让函数参数拥有默认值
当你在咖啡店点咖啡时,服务员会问:"要加糖吗?"如果你不回答,有的咖啡店会默认不加糖,有的会默认加一份糖。这个"默认行为"让点单流程更顺畅——你不必每次都明确说明每个细节。在 JavaScript 中,默认参数就提供了类似的便利,让函数在某些参数未提供时能够自动使用预设的默认值。
什么是默认参数
默认参数(Default Parameters)是 ES6 引入的特性,允许我们在函数声明时为参数指定默认值。如果调用函数时没有提供该参数,或者提供的值是 undefined,函数会使用默认值:
function greet(name = "Guest") {
console.log(`Hello, ${name}!`);
}
greet("Alice"); // Hello, Alice!
greet(); // Hello, Guest!在这个简单的例子中,如果调用 greet() 时没有传入 name 参数,它会使用默认值 "Guest"。这比传统的参数检查方式更简洁清晰。
ES6 之前的做法
在默认参数语法出现之前,我们需要手动检查参数并设置默认值:
// 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),也会被替换为默认值:
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 时生效,不会有这个问题:
function setCount(count = 10) {
console.log(`Count: ${count}`);
}
setCount(5); // Count: 5
setCount(0); // Count: 0 (正确处理了0)
setCount(); // Count: 10
setCount(undefined); // Count: 10默认参数的工作原理
默认参数只在两种情况下生效:
- 未传递该参数
- 显式传递
undefined
传递其他假值(如 null、0、false、"")都不会触发默认值:
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"。默认参数的设计遵循了这个语义:
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 不触发默认值多个默认参数
一个函数可以有多个默认参数,它们可以按任意组合使用:
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:
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 会影响可读性。在这种情况下,使用对象参数可能更好:
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. 函数调用作为默认值
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重要:默认参数表达式是在调用时计算的,而不是定义时:
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. 引用其他参数
默认参数可以引用前面的参数:
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" }但是,你不能引用后面的参数,因为它们还没有被初始化:
// ❌ 错误:不能引用后面的参数
// 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 103. 复杂表达式
默认参数可以是任何表达式,包括三元运算符、逻辑运算等:
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: $764. 对象和数组字面量
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与解构结合使用
默认参数与解构赋值结合使用时非常强大:
对象解构参数
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,导致错误:
// ❌ 没有默认空对象
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 }数组解构参数
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)与剩余参数结合
默认参数可以和剩余参数一起使用,但剩余参数不能有默认值(因为它总是一个数组):
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 请求配置
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. 数据格式化
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,2353. 创建 HTML 元素
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. 分页和过滤
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. 默认值是引用类型时要小心
如果默认值是对象或数组,它会在每次调用时被重用(除非重新计算):
// ⚠️ 潜在问题:共享同一个对象
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. 默认值的求值顺序
默认值从左到右求值,前面的参数可以被后面的引用:
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 03. 避免复杂的默认值表达式
虽然默认值可以是复杂表达式,但过于复杂会降低可读性:
// ❌ 过于复杂
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 不会触发默认值:
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. 必需参数在前,可选参数在后
将必需参数放在前面,有默认值的可选参数放在后面:
// ✅ 好:必需参数在前
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 开发的标准实践。配合解构和剩余参数,你可以创建非常灵活和优雅的函数接口。