JavaScript 函数默认参数
在构建函数时,我们经常希望它们具有灵活性。有时,函数需要某些特定的信息(实参)才能完成工作,但有时,如果某些信息没有提供也是可以的。
例如,想象一个计算最终价格的函数。它总是需要“基础价格”,但“折扣”或“税率”可能是可选的。如果用户没有指定折扣,我们希望它默认为 0%。如果他们没有指定税率,我们可能希望它默认为标准税率,比如 5%。
这就是 默认参数 (Default Parameters) 发挥作用的地方。
它们允许我们在函数定义中直接为参数分配一个后备值 (fallback value)。如果在函数调用期间没有传递该参数的实参,或者显式传递了 undefined,默认值就会生效。这使得我们的函数更加健壮且易于使用,而无需在函数体内编写复杂的条件检查。
这个现代 JavaScript 特性(在 ES6 中引入)显著地清理了我们的代码,并提供了一种清晰的方式来处理可选输入。
1. 理解默认参数
默认参数是 JavaScript 的一项功能,允许在调用函数时如果没有传递值(或传递了 undefined),则使用默认值初始化函数参数。这防止了错误,并使函数对缺失的参数具有更强的适应力,提供了一个“后备”值。
1.2 默认参数的语法
语法非常直观。你只需使用赋值运算符 (=) 在函数的参数列表中直接为参数分配默认值。
function greet(name = "访客") {
console.log(`你好, ${name}!`);
}在这个例子中,name 是一个参数,它的默认值是 "访客"。
1.3 默认参数是如何工作的
让我们分解一下 greet 函数在不同调用情况下的表现:
- 提供实参: 如果你调用
greet("爱丽丝"),name参数将采用值"爱丽丝",默认值"访客"将被忽略。
greet("爱丽丝"); // 输出: 你好, 爱丽丝!- 未提供实参: 如果你调用
greet(),没有为name提供值。在这种情况下,JavaScript 会自动将undefined赋值给name。当一个具有默认值的参数接收到undefined时,它的默认值就会被使用。所以,name变成了 "访客"。
greet(); // 输出: 你好, 访客!- 显式传递 undefined: 如果你显式地将
undefined作为实参传递,默认值也会被使用。
greet(undefined); // 输出: 你好, 访客!重要区别:undefined会触发默认值,但其他“假值 (falsy values)”如null、0或空字符串''不会。如果你传递null、0或'',这些值将被赋值给参数,而默认值不会被使用。
greet(null); // 输出: 你好, null!
greet(''); // 输出: 你好, !
greet(0); // 输出: 你好, 0!1.4 与 ES6 之前的写法对比
在 ES6 引入默认参数之前,开发者必须在函数体内手动检查缺失的参数。这通常涉及使用逻辑或运算符 (||) 或 if 语句。
ES6 之前的做法(使用 ||):
function calculateTotal(price, taxRate) {
// 如果 taxRate 是 undefined, null, 0, 或空字符串, 它将默认为 0.05
taxRate = taxRate || 0.05; // 这有问题,如果 0 是一个有效的税率怎么办!
return price * (1 + taxRate);
}
console.log(calculateTotal(100)); // 输出: 105 (taxRate 默认为 0.05)
console.log(calculateTotal(100, 0.10)); // 输出: 110
console.log(calculateTotal(100, 0)); // 输出: 105 (问题: 0 是有效税率,但 || 把它变成了默认值!)如你所见,|| 可能会导致意想不到的后果,因为它将 0(以及 null, '')视为“假值”并应用默认值,即使 0 原本是一个有效的输入。
ES6 之前的做法(使用 if 语句):
function calculateTotalImproved(price, taxRate) {
if (taxRate === undefined) { // 显式检查 undefined
taxRate = 0.05;
}
return price * (1 + taxRate);
}
console.log(calculateTotalImproved(100)); // 输出: 105
console.log(calculateTotalImproved(100, 0.10)); // 输出: 110
console.log(calculateTotalImproved(100, 0)); // 输出: 100 (正确, 0 现在被认可了)虽然这种 if 语句的方法比 || 更健壮,但它在函数体内增加了样板代码。默认参数为完全相同的逻辑提供了更清晰、更简洁的语法,而且直接写在函数签名中。
2. 参数的顺序
使用默认参数时,有一个约定:带有默认值的参数通常应该放在没有默认值的参数之后。虽然 JavaScript 允许你将默认参数放在任何位置,但这会导致困惑并使调用变得尴尬。
良好实践(非默认在前,默认在后):
function sendMessage(message, sender = "匿名者", recipient = "所有人") {
console.log(`来自: ${sender} 发送给: ${recipient} - 消息: "${message}"`);
}
sendMessage("你好世界!"); // 来自: 匿名者 发送给: 所有人 - 消息: "你好世界!"
sendMessage("重要公告", "管理员"); // 来自: 管理员 发送给: 所有人 - 消息: "重要公告"
sendMessage("私聊", "用户1", "用户2"); // 来自: 用户1 发送给: 用户2 - 消息: "私聊"尴尬的做法(默认参数在非默认参数之前):
// 这虽然能运行,但在不显式传递 undefined 给 sender 的情况下,你怎么调用它?
function badSendMessage(sender = "匿名者", message) {
console.log(`来自: ${sender} - 消息: "${message}"`);
}
// 要使用 sender 的默认值,你必须为第一个参数显式传递 undefined
badSendMessage(undefined, "你好"); // 来自: 匿名者 - 消息: "你好"
badSendMessage("管理员", "你好"); // 来自: 管理员 - 消息: "你好"
// badSendMessage("你好"); // 这会将 "你好" 赋值给 sender,而 message 将是 undefined,导致 "来自: 你好 - 消息: undefined"如 badSendMessage 所示,如果 sender 有默认值但放在了没有默认值的 message 之前,想要使用 sender 的默认值但提供 message 时,就需要显式地为 sender 传递 undefined。这使得函数调用变得不直观。请坚持将默认参数放在参数列表的末尾。
3. 使用表达式作为默认值
默认参数值不仅限于简单的字面量(如数字或字符串)。你可以使用任何有效的 JavaScript 表达式,包括函数调用、变量引用,甚至是在列表中排在前面的其他参数。只有在需要参数的默认值时,表达式才会被计算(执行)。
function createId(base = Math.random().toString(36).substring(2, 9)) {
console.log(`生成的 ID: ${base}`);
}
createId(); // 输出: 生成的 ID: [随机字符串_1]
createId(); // 输出: 生成的 ID: [随机字符串_2] (每次都是新的随机字符串)
createId("user-123"); // 输出: 生成的 ID: user-123在这个例子中,Math.random().toString(36).substring(2, 9) 是一个生成随机字符串的表达式。只有当 createId() 被调用且没有参数时,它才会被执行。
你也可以在默认参数表达式中使用前面定义的参数:
function greetUser(firstName, lastName = "Doe", fullName = `${firstName} ${lastName}`) {
console.log(`你好, ${fullName}!`);
}
greetUser("John"); // 输出: 你好, John Doe!
greetUser("Jane", "Smith"); // 输出: 你好, Jane Smith!
greetUser("Alice", "Wonderland", "Alice L. Wonderland"); // 输出: 你好, Alice L. Wonderland!
// 注意 fullName 如何使用了 firstName 和 lastName。只有当 fullName 是 undefined 时,它才会被计算。在这里,fullName 的默认值依赖于 firstName 和 lastName。这展示了默认参数的强大功能和灵活性。
4. 实战示例与演示
让我们看一些更全面的例子,了解如何使用默认参数来创建灵活且健壮的函数。
4.1 示例 1:配置用户个人资料显示
想象一个生成用户个人资料显示名称和头像的函数。有些详细信息可能是可选的。
/**
* 生成用户个人资料显示字符串。
* @param {string} username - 用户的唯一用户名(必需)。
* @param {string} displayName - 可选的公开显示名称。默认为用户名。
* @param {string} avatarUrl - 可选的用户头像图片 URL。默认为通用占位符。
* @param {boolean} isAdmin - 可选标志,指示用户是否为管理员。默认为 false。
*/
function createUserProfileDisplay(username, displayName = username, avatarUrl = "/images/default-avatar.png", isAdmin = false) {
let adminTag = isAdmin ? " (管理员)" : "";
console.log(`--- 用户个人资料 ---`);
console.log(`用户名: ${username}`);
console.log(`显示名称: ${displayName}${adminTag}`);
console.log(`头像: ${avatarUrl}`);
console.log(`--------------------`);
console.log('\n'); // 添加换行符,以便在调用之间更易读
}
// 情况 1: 仅提供必需的用户名
createUserProfileDisplay("john_doe");
/* 输出:
--- 用户个人资料 ---
用户名: john_doe
显示名称: john_doe
头像: /images/default-avatar.png
--------------------
*/
// 情况 2: 提供用户名和自定义显示名称
createUserProfileDisplay("jane_smith", "Jane S.");
/* 输出:
--- 用户个人资料 ---
用户名: jane_smith
显示名称: Jane S.
头像: /images/default-avatar.png
--------------------
*/
// 情况 3: 提供用户名、自定义显示名称和自定义头像
createUserProfileDisplay("coder_x", "代码大师", "https://example.com/coder-x-avatar.jpg");
/* 输出:
--- 用户个人资料 ---
用户名: coder_x
显示名称: 代码大师
头像: https://example.com/coder-x-avatar.jpg
--------------------
*/
// 情况 4: 提供用户名、自定义显示名称、自定义头像和管理员状态
createUserProfileDisplay("admin_user", "超级管理员", "https://example.com/admin-avatar.jpg", true);
/* 输出:
--- 用户个人资料 ---
用户名: admin_user
显示名称: 超级管理员 (管理员)
头像: https://example.com/admin-avatar.jpg
--------------------
*/
// 情况 5: 显式使用 `undefined` 来触发 avatarUrl 的默认值
createUserProfileDisplay("test_user", "测试用户", undefined, false);
/* 输出:
--- 用户个人资料 ---
用户名: test_user
显示名称: 测试用户
头像: /images/default-avatar.png
--------------------
*/这个例子清楚地展示了默认参数如何使函数高度灵活。你只需要提供 username,其他字段就会智能地回退到合理的默认值。
4.2 示例 2:构建绘图工具的配置函数
考虑一个在画布上绘制形状的函数。它需要坐标,但可以有可选属性,如颜色、线宽和填充状态。
/**
* 在假设的画布上绘制形状。
* @param {number} x - 形状的 X 坐标(必需)。
* @param {number} y - 形状的 Y 坐标(必需)。
* @param {string} shapeType - 要绘制的形状类型。默认为 "圆形"。
* @param {string} color - 形状的描边颜色。默认为 "黑色"。
* @param {number} lineWidth - 线条的宽度。默认为 1。
* @param {boolean} fill - 是否填充形状。默认为 false (不填充)。
*/
function drawShape(x, y, shapeType = "圆形", color = "黑色", lineWidth = 1, fill = false) {
console.log(`在 (${x}, ${y}) 处绘制一个 ${shapeType}:`);
console.log(` - 颜色: ${color}`);
console.log(` - 线宽: ${lineWidth}`);
console.log(` - 填充: ${fill ? '是' : '否'}`);
console.log('\n');
}
// 情况 1: 最小化绘制 - 在 (10, 20) 处绘制黑色圆形,默认线宽,不填充
drawShape(10, 20);
/* 输出:
在 (10, 20) 处绘制一个 圆形:
- 颜色: 黑色
- 线宽: 1
- 填充: 否
*/
// 情况 2: 绘制一个红色矩形,线条更粗
drawShape(50, 60, "矩形", "红色", 3);
/* 输出:
在 (50, 60) 处绘制一个 矩形:
- 颜色: 红色
- 线宽: 3
- 填充: 否
*/
// 情况 3: 绘制一个填充的蓝色正方形
drawShape(100, 120, "正方形", "蓝色", 2, true);
/* 输出:
在 (100, 120) 处绘制一个 正方形:
- 颜色: 蓝色
- 线宽: 2
- 填充: 是
*/
// 情况 4: 仅更改线宽,保持默认的形状类型和颜色
drawShape(150, 180, undefined, undefined, 5); // 必须为 shapeType 和 color 传递 undefined 以跳过它们
/* 输出:
在 (150, 180) 处绘制一个 圆形:
- 颜色: 黑色
- 线宽: 5
- 填充: 否
*/这演示了具有许多可选配置参数的常见模式。当你需要跳过几个默认参数以便为列表后面的参数提供值时,必须为你想要跳过的参数显式传递 undefined。这突显了为什么将默认参数放在最后是一个好习惯。