Skip to content

剩余参数:优雅地处理不定数量的参数

想象你在组织一次聚会,你知道你的好朋友 Alice 和 Bob 一定会来,但除此之外还会有多少人参加呢?你不确定。在编写邀请函时,你可能会写"Alice、Bob 以及其他所有朋友"。在 JavaScript 中,剩余参数就像这个"其他所有朋友"——它让我们能够优雅地处理数量不确定的额外参数。

什么是剩余参数

剩余参数(Rest Parameters)是 ES6 引入的一个特性,使用三个点(...)后跟参数名来表示。它会将所有"剩余"的参数收集到一个真正的数组中:

javascript
function sum(...numbers) {
  console.log(numbers); // numbers 是一个真正的数组
  console.log(Array.isArray(numbers)); // true

  let total = 0;
  for (let num of numbers) {
    total += num;
  }
  return total;
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(5, 10, 15, 20)); // 50
console.log(sum()); // 0

在这个例子中,...numbers 会捕获所有传入的参数,并将它们放入一个名为 numbers 的数组。无论传入多少个参数,它们都会被收集到这个数组中。

剩余参数与我们之前学习的 arguments 对象类似,但有几个重要的改进:

javascript
function compareRestAndArguments(...restParams) {
  console.log("=== 剩余参数 ===");
  console.log(restParams); // 真正的数组
  console.log(Array.isArray(restParams)); // true

  console.log("\n=== arguments 对象 ===");
  console.log(arguments); // 类数组对象
  console.log(Array.isArray(arguments)); // false
}

compareRestAndArguments(1, 2, 3, 4, 5);

剩余参数 vs arguments 对象

虽然剩余参数和 arguments 都能访问函数的参数,但剩余参数在许多方面更胜一筹:

1. 真正的数组 vs 类数组对象

剩余参数是一个真正的数组,可以直接使用所有数组方法:

javascript
function findLongestWord(...words) {
  // 可以直接使用数组方法
  return words.reduce((longest, current) => {
    return current.length > longest.length ? current : longest;
  }, "");
}

console.log(findLongestWord("JavaScript", "Python", "Go")); // JavaScript
console.log(findLongestWord("apple", "banana", "cherry", "pineapple")); // pineapple

arguments 需要先转换成数组:

javascript
function findLongestWordOldWay() {
  // 必须先转换
  let words = Array.from(arguments);
  return words.reduce((longest, current) => {
    return current.length > longest.length ? current : longest;
  }, "");
}

console.log(findLongestWordOldWay("JavaScript", "Python", "Go")); // JavaScript

2. 可以与命名参数配合使用

剩余参数可以与普通参数组合,但必须放在最后:

javascript
function logMessage(level, timestamp, ...messages) {
  console.log(`[${level}] ${timestamp}`);
  messages.forEach((msg) => console.log(`  - ${msg}`));
}

logMessage(
  "ERROR",
  "2025-12-04 10:00:00",
  "Connection failed",
  "Retrying...",
  "Timeout after 30s"
);
// 输出:
// [ERROR] 2025-12-04 10:00:00
//   - Connection failed
//   - Retrying...
//   - Timeout after 30s

在这个例子中,leveltimestamp 是明确的命名参数,而 ...messages 捕获了所有剩余的参数。这使得代码的意图更加清晰——前两个参数有特殊含义,其余参数都是消息内容。

3. 箭头函数中的支持

剩余参数可以在箭头函数中使用,而 arguments 不行:

javascript
// ✅ 剩余参数:可以在箭头函数中使用
const multiply = (...numbers) => {
  return numbers.reduce((product, num) => product * num, 1);
};

console.log(multiply(2, 3, 4)); // 24
console.log(multiply(5, 10)); // 50

// ❌ arguments:箭头函数中不可用
const multiplyOldWay = () => {
  // ReferenceError: arguments is not defined
  // return Array.from(arguments).reduce((product, num) => product * num, 1);
};

4. 语义清晰

从函数签名就能看出函数接受可变数量的参数:

javascript
// ✅ 一眼就能看出这个函数接受多个数字参数
function average(...numbers) {
  if (numbers.length === 0) return 0;
  let sum = numbers.reduce((total, num) => total + num, 0);
  return sum / numbers.length;
}

// ❌ 看不出这个函数接受什么参数
function averageOldWay() {
  // 需要查看函数体才知道使用了 arguments
  if (arguments.length === 0) return 0;
  let sum = 0;
  for (let num of arguments) {
    sum += num;
  }
  return sum / arguments.length;
}

console.log(average(10, 20, 30, 40)); // 25

剩余参数的语法规则

1. 必须是最后一个参数

剩余参数必须放在参数列表的最后,因为它会"吞掉"所有剩余的参数:

javascript
// ✅ 正确:剩余参数在最后
function createUser(name, age, ...roles) {
  console.log(`Name: ${name}`);
  console.log(`Age: ${age}`);
  console.log(`Roles: ${roles.join(", ")}`);
}

createUser("Alice", 25, "admin", "editor", "moderator");
// Name: Alice
// Age: 25
// Roles: admin, editor, moderator

// ❌ 错误:剩余参数不能在中间或开始
// function invalid(...items, last) {} // SyntaxError
// function invalid(first, ...middle, last) {} // SyntaxError

2. 一个函数只能有一个剩余参数

你不能在一个函数中使用多个剩余参数:

javascript
// ❌ 错误:不能有多个剩余参数
// function invalid(...args1, ...args2) {} // SyntaxError

// ✅ 正确:只有一个剩余参数
function processData(action, ...items) {
  console.log(`Action: ${action}`);
  console.log(`Items count: ${items.length}`);
}

processData("update", "item1", "item2", "item3");
// Action: update
// Items count: 3

3. 剩余参数始终是数组

即使没有传入任何"剩余"参数,剩余参数也会是一个空数组,而不是 undefined:

javascript
function greet(greeting, ...names) {
  console.log(greeting);
  console.log(names); // 即使没有传入 names,也是 []
  console.log(names.length); // 0
}

greet("Hello");
// Hello
// []
// 0

greet("Hi", "Alice", "Bob");
// Hi
// ["Alice", "Bob"]
// 2

剩余参数的实际应用

1. 数学运算函数

剩余参数非常适合创建可以处理任意数量数值的函数:

javascript
function max(...numbers) {
  if (numbers.length === 0) {
    return -Infinity;
  }
  return Math.max(...numbers);
}

function min(...numbers) {
  if (numbers.length === 0) {
    return Infinity;
  }
  return Math.min(...numbers);
}

function range(...numbers) {
  return max(...numbers) - min(...numbers);
}

console.log(max(5, 12, 3, 9)); // 12
console.log(min(5, 12, 3, 9)); // 3
console.log(range(5, 12, 3, 9)); // 9

2. 格式化字符串

创建灵活的日志和格式化函数:

javascript
function formatHTML(tag, ...children) {
  let content = children.join("");
  return `<${tag}>${content}</${tag}>`;
}

function createList(...items) {
  let listItems = items.map((item) => `<li>${item}</li>`).join("");
  return `<ul>${listItems}</ul>`;
}

console.log(formatHTML("h1", "Welcome to JavaScript"));
// <h1>Welcome to JavaScript</h1>

console.log(formatHTML("p", "This is a ", "multi-part", " paragraph."));
// <p>This is a multi-part paragraph.</p>

console.log(createList("Apple", "Banana", "Cherry"));
// <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>

3. 组合多个数组

剩余参数可以用来合并多个数组:

javascript
function mergeArrays(...arrays) {
  return arrays.flat(); // 使用 flat() 展平一层
}

function mergeUnique(...arrays) {
  let merged = arrays.flat();
  return [...new Set(merged)]; // 去重
}

let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let arr3 = [7, 8, 9];

console.log(mergeArrays(arr1, arr2, arr3));
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

let list1 = [1, 2, 3];
let list2 = [3, 4, 5];
let list3 = [5, 6, 7];

console.log(mergeUnique(list1, list2, list3));
// [1, 2, 3, 4, 5, 6, 7]

4. 包装函数和中间件

剩余参数在创建包装函数时特别有用:

javascript
function measureTime(fn, ...args) {
  console.log(`Calling function with arguments: ${args}`);
  let start = Date.now();
  let result = fn(...args);
  let end = Date.now();
  console.log(`Execution time: ${end - start}ms`);
  return result;
}

function slowFunction(n) {
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += Math.sqrt(i);
  }
  return result;
}

measureTime(slowFunction, 1000000);
// Calling function with arguments: 1000000
// Execution time: 15ms (实际时间会变化)

5. 部分应用函数

创建返回新函数的高阶函数:

javascript
function partial(fn, ...fixedArgs) {
  return function (...remainingArgs) {
    return fn(...fixedArgs, ...remainingArgs);
  };
}

function greet(greeting, name, punctuation) {
  return `${greeting}, ${name}${punctuation}`;
}

// 创建一个固定了问候语的函数
let sayHello = partial(greet, "Hello");
console.log(sayHello("Alice", "!")); // Hello, Alice!
console.log(sayHello("Bob", ".")); // Hello, Bob.

// 创建一个固定了问候语和标点的函数
let sayHelloExcited = partial(greet, "Hello", "Alice");
console.log(sayHelloExcited("!!!")); // Hello, Alice!!!

剩余参数与扩展运算符的配合

剩余参数使用 ... 语法来收集参数,而扩展运算符同样使用 ... 语法来展开数组或对象。它们经常一起使用:

javascript
function addAndMultiply(multiplier, ...numbers) {
  // 剩余参数收集所有数字
  let sum = numbers.reduce((total, num) => total + num, 0);
  return sum * multiplier;
}

let values = [1, 2, 3, 4, 5];
// 扩展运算符展开数组
console.log(addAndMultiply(2, ...values)); // 30 ((1+2+3+4+5) * 2)

创建数组操作的实用函数:

javascript
function removeItems(array, ...itemsToRemove) {
  return array.filter((item) => !itemsToRemove.includes(item));
}

let fruits = ["apple", "banana", "cherry", "date", "elderberry"];
let filtered = removeItems(fruits, "banana", "date");
console.log(filtered); // ["apple", "cherry", "elderberry"]

结合使用创建强大的函数组合:

javascript
function compose(...functions) {
  return function (initialValue) {
    return functions.reduceRight((value, fn) => fn(value), initialValue);
  };
}

const double = (x) => x * 2;
const addTen = (x) => x + 10;
const square = (x) => x * x;

const combined = compose(square, addTen, double);
console.log(combined(5)); // 400
// 计算过程: 5 -> double -> 10 -> addTen -> 20 -> square -> 400

常见问题与最佳实践

1. 参数验证

剩余参数收集的是数组,记得验证数组内容:

javascript
function sum(...numbers) {
  // 验证所有参数都是数字
  for (let num of numbers) {
    if (typeof num !== "number") {
      throw new Error(`Expected number, got ${typeof num}`);
    }
  }

  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3)); // 6
try {
  console.log(sum(1, "2", 3)); // 抛出错误
} catch (error) {
  console.log(error.message); // Expected number, got string
}

2. 空数组处理

剩余参数可能是空数组,要处理这种情况:

javascript
function getFirst(...items) {
  if (items.length === 0) {
    return undefined;
  }
  return items[0];
}

console.log(getFirst()); // undefined
console.log(getFirst(10, 20, 30)); // 10

3. 明确命名参数 vs 剩余参数

当某些参数有特殊含义时,使用明确的命名参数:

javascript
// ✅ 好:第一个参数有特殊含义
function join(separator, ...strings) {
  return strings.join(separator);
}

console.log(join(" - ", "apple", "banana", "cherry"));
// apple - banana - cherry

// ❌ 不够清晰:所有参数都在剩余参数中
function joinUnclear(...args) {
  let separator = args[0];
  let strings = args.slice(1);
  return strings.join(separator);
}

4. 性能考虑

虽然剩余参数很方便,但它会创建新数组。在性能敏感的代码中要注意:

javascript
// 如果函数会被频繁调用,剩余参数的数组创建可能有性能影响
function frequentlyCalledFunction(...args) {
  // 每次调用都会创建新数组
}

// 对于固定数量的参数,直接命名更高效
function efficientFunction(a, b, c) {
  // 不创建额外的数组
  return a + b + c;
}

5. 与解构结合使用

剩余参数可以与数组解构结合:

javascript
function processValues(action, ...[first, second, ...rest]) {
  console.log(`Action: ${action}`);
  console.log(`First: ${first}`);
  console.log(`Second: ${second}`);
  console.log(`Rest: ${rest}`);
}

processValues("update", 1, 2, 3, 4, 5);
// Action: update
// First: 1
// Second: 2
// Rest: [3, 4, 5]

不过这种写法可读性较差,通常不推荐:

javascript
// ✅ 更清晰的写法
function processValues(action, ...values) {
  let [first, second, ...rest] = values;
  console.log(`Action: ${action}`);
  console.log(`First: ${first}`);
  console.log(`Second: ${second}`);
  console.log(`Rest: ${rest}`);
}

总结

剩余参数是 ES6 带来的重要特性,它为处理可变数量的参数提供了现代化、优雅的解决方案。

关键要点:

  • 剩余参数使用 ...语法,将多个参数收集到一个真正的数组
  • 剩余参数必须是参数列表的最后一个参数
  • 一个函数只能有一个剩余参数
  • 剩余参数可以在箭头函数中使用,而 arguments 不行
  • 剩余参数比 arguments 对象更清晰、更灵活
  • 可以与普通命名参数配合使用,增强代码可读性
  • 适用于数学运算、数组操作、函数组合等多种场景
  • 要注意参数验证和空数组处理
  • 与扩展运算符配合使用可以实现强大的功能

剩余参数是现代 JavaScript 开发的标准做法。相比传统的 arguments 对象,它语法更清晰,功能更强大,应该成为你处理可变参数的首选方案。在下一章中,我们将学习默认参数,进一步提升函数参数处理的灵活性。