Javascript 零基础教程

JavaScript 函数参数顺序与类型

在之前的模块中,我们探讨了 JavaScript 函数的基础知识:如何声明、调用以及理解基本的作用域。现在,我们将深入探讨函数如何接收和处理信息。

我们发送给函数的值称为 实参 (Arguments),而函数定义用来接收这些值的占位符称为 形参 (Parameters)

要让函数按预期正确工作,关键在于理解 JavaScript 如何将实参和形参进行匹配,特别是它们的顺序以及传递数据的类型所带来的影响。掌握这一点可以确保你的函数处理正确的数据,并避免导致代码 bug 的常见陷阱。

1. 参数顺序的重要性

当你定义一个带参数的函数时,列出参数的顺序非常重要。JavaScript 严格根据位置将你传递的实参与函数的形参进行匹配。

你提供的第一个实参会被赋值给第一个形参,第二个实参赋值给第二个形参,依此类推。这种位置匹配是函数接收和处理数据的基础。

1.2 位置映射详解

想象你的函数是一个包含特定字段的表单。每个形参都是一个字段标签,而你提供的实参就是填入该字段的数据。如果你在“姓氏”字段中填入你的名字,表单虽然能接收数据,但无法理解你的真实意图。

JavaScript 的工作原理类似:在匹配时,它并不关心形参的名称,它只关心位置

让我们用一个例子来说明:

// 定义带有两个形参的函数:'firstName'(名) 和 'lastName'(姓)
function greetUser(firstName, lastName) {
  console.log(`你好, ${firstName} ${lastName}!`);
}

// 调用函数
greetUser("爱丽丝", "史密斯");
// 输出: 你好, 爱丽丝 史密斯!
// "爱丽丝" (第1个实参) 映射到 'firstName' (第1个形参)
// "史密斯" (第2个实参) 映射到 'lastName' (第2个形参)

greetUser("鲍勃", "约翰逊");
// 输出: 你好, 鲍勃 约翰逊!

greetUser 函数中,firstName 是第一个形参,lastName 是第二个。当我们调用 greetUser("爱丽丝", "史密斯") 时,“爱丽丝”作为第一个实参被赋值给 firstName,“史密斯”作为第二个实参被赋值给 lastName。输出结果正确反映了这种映射。

现在,看看如果我们改变实参的顺序会发生什么:

function greetUser(firstName, lastName) {
  console.log(`你好, ${firstName} ${lastName}!`);
}

greetUser("史密斯", "爱丽丝");
// 输出: 你好, 史密斯 爱丽丝!
// "史密斯" (第1个实参) 仍然映射到 'firstName'
// "爱丽丝" (第2个实参) 仍然映射到 'lastName'

尽管直觉上“史密斯”是姓,“爱丽丝”是名,但 JavaScript 不会做这种假设。它严格遵守顺序。因为它们在参数列表中的位置,导致“史密斯”变成了 firstName 而“爱丽丝”变成了 lastName。这凸显了为什么理解和遵守参数顺序对于函数逻辑的正确性至关重要。

2. 处理缺失的参数

如果你调用函数时,提供的实参数量少于函数期望的形参数量,会发生什么?

JavaScript 在这种情况下不会报错。相反,任何没有对应实参的形参都会自动被赋值为 undefined

来看一个例子:

function displayDetails(name, age, city) {
  console.log(`姓名: ${name}`);
  console.log(`年龄: ${age}`);
  console.log(`城市: ${city}`);
}

// 情况 1: 提供所有参数
displayDetails("张三", 30, "北京");
/*
输出:
姓名: 张三
年龄: 30
城市: 北京
*/

// 情况 2: 缺失一个参数 (city)
displayDetails("李四", 25);
/*
输出:
姓名: 李四
年龄: 25
城市: undefined
*/
// 'city' 形参接收到 'undefined',因为没有提供第三个实参。

// 情况 3: 缺失更多参数
displayDetails("王五");
/*
输出:
姓名: 王五
年龄: undefined
城市: undefined
*/
// 'age' 和 'city' 形参都接收到 'undefined'。

这种行为很重要,因为如果你的函数没有设计好如何处理 undefined,它可能会导致意外结果。例如,尝试对 undefined 进行算术运算会导致 NaN (Not a Number,非数字)。检查 undefined 或提供后备值(我们将在未来的课程中学习默认参数)通常是一个好习惯。

3. 处理多余的参数

反之,如果你提供的实参数量多于函数的形参数量呢?

JavaScript 会简单地忽略多余的实参。它们在内部仍然被传递给了函数(可以通过一个称为 arguments 对象的旧特性访问,我们稍后会讨论),但它们不会直接赋值给任何命名的形参。

function calculateSum(num1, num2) {
  console.log(`第一个数字: ${num1}`);
  console.log(`第二个数字: ${num2}`);
  return num1 + num2;
}

const result = calculateSum(10, 20, 30, 40);
console.log(`总和: ${result}`);

/*
输出:
第一个数字: 10
第二个数字: 20
总和: 30
*/
// '30' 和 '40' 是多余的实参,被命名形参 'num1' 和 'num2' 忽略了。

虽然多余的参数被命名形参忽略了,但这并不意味着它们不存在。只是它们没有映射到显式的参数名称上。对于大多数现代 JavaScript 开发,最佳实践是设计实参数量与形参数量精确匹配的函数。提供多余参数通常意味着对函数预期输入的误解,或者是时候重构函数以更结构化的方式接收参数了(例如,对于许多相关参数使用对象)。

4. 理解参数类型

与其他一些编程语言不同,JavaScript 是动态类型 (Dynamically Typed) 的。这意味着你在定义函数时不需要声明参数的数据类型。

一个参数可以接收任何类型的数据:数字、字符串、布尔值、对象、数组、函数,甚至是 nullundefined。虽然这种灵活性很强大,但也意味着开发者有责任确保函数接收和处理的是它期望的数据类型。

4.1 动态类型的实际应用

让我们看看 JavaScript 如何处理传递给同一个函数形参的不同数据类型。

function processData(input) {
  console.log(`接收到的输入: ${input}`);
  console.log(`输入类型: ${typeof input}`);

  // 尝试根据假设的类型执行操作
  if (typeof input === 'number') {
    console.log(`双倍结果: ${input * 2}`);
  } else if (typeof input === 'string') {
    console.log(`大写输入: ${input.toUpperCase()}`);
  } else {
    console.log("输入既不是数字也不是字符串,不执行特定操作。");
  }
  console.log('---');
}

processData(10);
/*
输出:
接收到的输入: 10
输入类型: number
双倍结果: 20
---
*/

processData("你好");
/*
输出:
接收到的输入: 你好
输入类型: string
大写输入: 你好
---
*/

processData(true);
/*
输出:
接收到的输入: true
输入类型: boolean
输入既不是数字也不是字符串,不执行特定操作。
---
*/

processData([1, 2, 3]);
/*
输出:
接收到的输入: 1,2,3
输入类型: object
输入既不是数字也不是字符串,不执行特定操作。
---
*/

processData(null);
/*
输出:
接收到的输入: null
输入类型: object
输入既不是数字也不是字符串,不执行特定操作。
---
*/

如你所见,processData 中的 input 参数可以成功接收并报告各种类型的值。typeof 运算符是一个非常有用的工具,可以在运行时检查变量的类型,这有助于编写更健壮的函数来适应不同的输入或验证它们。

4.2 类型强制转换 (Type Coercion) 与潜在问题

JavaScript 中一个比较微妙的地方是类型强制转换。这是指当一个操作期望某种数据类型时,JavaScript 会自动将一种数据类型转换为另一种。虽然这在某些情况下很方便,但也可能导致意外行为。

考虑一个设计用来将两个数字相加的函数:

function addNumbers(a, b) {
  console.log(`a 的类型: ${typeof a}, b 的类型: ${typeof b}`);
  return a + b;
}

console.log(`结果 1: ${addNumbers(5, 3)}`);
// 输出: a 的类型: number, b 的类型: number, 结果 1: 8 (正确)

console.log(`结果 2: ${addNumbers("5", 3)}`);
// 输出: a 的类型: string, b 的类型: number, 结果 2: 53 (加法错误!)
// JavaScript 将数字 3 强制转换为字符串 "3",并执行了字符串拼接。

console.log(`结果 3: ${addNumbers(5, "3")}`);
// 输出: a 的类型: number, b 的类型: string, 结果 3: 53 (加法错误!)

console.log(`结果 4: ${addNumbers("你好", " 世界")}`);
// 输出: a 的类型: string, b 的类型: string, 结果 4: 你好 世界 (拼接正确)

console.log(`结果 5: ${addNumbers(true, 1)}`);
// 输出: a 的类型: boolean, b 的类型: number, 结果 5: 2
// 'true' 被强制转换为 1。

结果 2结果 3 中,尽管函数名 addNumbers 暗示了数值加法,但传入字符串参数导致了字符串拼接。这是因为 JavaScript 中的 + 运算符根据操作数的类型执行加法或拼接。如果至少有一个操作数是字符串,它就会执行拼接。

为了避免此类问题(特别是在数值运算中),如果预计会有不同的输入,最好的做法是将输入显式转换为预期的类型,或者进行验证。对于数字,你可以使用 Number()parseInt()parseFloat() 函数。

function strictAddNumbers(a, b) {
  const numA = Number(a); // 显式转换为数字
  const numB = Number(b); // 显式转换为数字

  // 检查转换是否得到有效数字
  if (isNaN(numA) || isNaN(numB)) {
    console.error("错误: 两个参数都必须能转换为数字。");
    return NaN; // 指示无效结果
  }

  console.log(`严格加法: numA 类型: ${typeof numA}, numB 类型: ${typeof numB}`);
  return numA + numB;
}

console.log(`严格结果 1: ${strictAddNumbers(5, 3)}`);       // 输出: 8
console.log(`严格结果 2: ${strictAddNumbers("5", 3)}`);     // 输出: 8
console.log(`严格结果 3: ${strictAddNumbers(5, "3")}`);     // 输出: 8
console.log(`严格结果 4: ${strictAddNumbers("abc", 3)}`);   // 输出: 错误: 两个参数都必须能转换为数字。 NaN

这种显式的类型转换和验证使得 strictAddNumbers 函数更加健壮和可预测。

4.3 参数类型的最佳实践

  • 意图明确: 清晰地命名你的参数,以表明它们期望接收什么类型的数据(例如,userName, userAge, isValid)。
  • 验证输入: 对于执行关键操作或与外部系统交互的函数,考虑在函数开头添加检查,确保参数符合预期类型。你可以使用 typeofArray.isArray()instanceof 或自定义验证逻辑。
  • 文档说明: 尤其对于大型项目或共享代码库,使用注释或 JSDoc 记录每个参数的预期类型和用途。
  • 使用严格相等 (===): 在函数内比较值时,始终使用 ===(严格相等)而不是 ==(宽松相等),以避免比较过程中发生意外的类型强制转换。

通过留意参数类型并结合验证,你可以显著提高 JavaScript 代码的可靠性和可维护性。

5. 详尽的实战示例与演示

让我们结合对参数顺序和类型的理解,来看一些更全面的例子。

5.1 示例 1:创建用户个人资料摘要

我们将创建一个接收用户详细信息并生成摘要的函数。这个函数非常依赖正确的参数顺序。

/**
 * 生成用户个人资料的摘要字符串。
 * 期望参数按特定顺序排列。
 * @param {string} name - 用户的全名。
 * @param {number} age - 用户的年龄。
 * @param {string} email - 用户的电子邮件地址。
 * @param {boolean} isActive - 用户账户是否激活。
 * @returns {string} 格式化后的用户资料摘要。
 */
function createUserSummary(name, age, email, isActive) {
  // 验证输入类型(增强健壮性的好习惯)
  if (typeof name !== 'string' || name.trim() === '') {
    return "错误: 姓名必须是非空字符串。";
  }
  if (typeof age !== 'number' || age <= 0) {
    return "错误: 年龄必须是正数。";
  }
  if (typeof email !== 'string' || !email.includes('@')) {
    return "错误: 邮箱必须是有效的邮箱字符串。";
  }
  if (typeof isActive !== 'boolean') {
    return "错误: isActive 必须是布尔值。";
  }

  const status = isActive ? "激活" : "未激活";
  return `用户: ${name}, 年龄: ${age}, 邮箱: ${email}, 状态: ${status}`;
}

// 正确用法: 所有参数均按预期顺序和类型提供
console.log(createUserSummary("张三", 30, "zhangsan@example.com", true));
// 输出: 用户: 张三, 年龄: 30, 邮箱: zhangsan@example.com, 状态: 激活

// 'age' 和 'email' 顺序错误 - 将导致验证中的类型错误
console.log(createUserSummary("李四", "lisi@example.com", 25, false));
// 输出: 错误: 年龄必须是正数。
// 解释: "lisi@example.com" 被作为 'age' 传入,它是一个字符串,未通过年龄验证。

// 缺失一个参数 - 'isActive' 将是 undefined,导致验证失败
console.log(createUserSummary("王五", 45, "wangwu@example.com"));
// 输出: 错误: isActive 必须是布尔值。
// 解释: 第三个实参 "wangwu@example.com" 映射到了 'email'。
// 第四个形参 'isActive' 接收到 `undefined`,未通过类型检查。

// 提供多余参数 - 将被命名形参忽略
console.log(createUserSummary("赵六", 28, "zhaoliu@example.com", true, "额外数据"));
// 输出: 用户: 赵六, 年龄: 28, 邮箱: zhaoliu@example.com, 状态: 激活
// 解释: "额外数据" 是多余实参,被忽略了。

这个例子清楚地展示了严格遵守参数顺序和类型是多么重要,尤其是在包含验证的情况下。验证检查可以捕捉因类型错误或参数缺失而产生的问题,使函数更加健壮。

5.2 示例 2:计算几何面积(含潜在强制转换问题)

让我们构建一个计算矩形面积的函数。这突显了类型敏感性。

/**
 * 计算矩形的面积。
 * @param {number} width - 矩形的宽度。
 * @param {number} height - 矩形的高度。
 * @returns {number|string} 面积,如果输入无效则返回错误消息。
 */
function calculateRectangleArea(width, height) {
  // 基本类型检查和验证
  if (typeof width !== 'number' || typeof height !== 'number') {
    return "错误: 宽度和高度都必须是数字。";
  }
  if (width <= 0 || height <= 0) {
    return "错误: 宽度和高度都必须是正数。";
  }
  return width * height;
}

// 正确用法
console.log(`面积 1: ${calculateRectangleArea(10, 5)}`); // 输出: 面积 1: 50

// 'height' 类型不正确
console.log(`面积 2: ${calculateRectangleArea(7, "4")}`);
// 输出: 面积 2: 错误: 宽度和高度都必须是数字。
// 解释: 验证捕获了 "4" 是字符串,防止了与字符串相乘。

// 正确用法(浮点数)
console.log(`面积 3: ${calculateRectangleArea(12.5, 8.2)}`); // 输出: 面积 3: 102.5

// 缺失一个参数
console.log(`面积 4: ${calculateRectangleArea(6)}`);
// 输出: 面积 4: 错误: 宽度和高度都必须是数字。
// 解释: 'height' 是 undefined,不是 'number'。

在这个函数中,typeof 检查至关重要。如果没有它们,calculateRectangleArea(7, "4") 可能会尝试 7 * "4"。JavaScript 可能会将 "4" 强制转换为 4 并返回 28,这看起来是正确的,但绕过了预期的类型安全。我们的显式类型检查防止了这种隐式强制转换,并在输入类型无效时返回错误消息,从而使函数更可预测且更安全。