Javascript 零基础教程

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)”如 null0 或空字符串 ''不会。如果你传递 null0'',这些值将被赋值给参数,而默认值不会被使用。
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 的默认值依赖于 firstNamelastName。这展示了默认参数的强大功能和灵活性。

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。这突显了为什么将默认参数放在最后是一个好习惯。