JavaScript 的函数参数机制看似简单,实则在 ES6 之后有了质的飞跃。理解默认参数、剩余参数、arguments 对象之间的关系和区别,是写出健壮函数的必备基础。
9.2.1 默认参数(Default Parameters)
在 ES6 之前,我们只能用逻辑或 || 来模拟默认值,但这种写法有严重缺陷:无法区分 false、0、"" 等“假值”与真正的“未传参”。
// 旧写法:逻辑或
function greet(name) {
name = name || "陌生人";
console.log("你好," + name);
}
greet(""); // 输出 "你好,陌生人" —— 这显然不符合预期
ES6 引入了真正的默认参数语法,只有当参数为 undefined 时才会触发默认值:
function greet(name = "陌生人") {
console.log(`你好,${name}`);
}
greet(); // 你好,陌生人
greet(undefined); // 你好,陌生人
greet(""); // 你好,(空字符串)
greet(0); // 你好,0
greet(false); // 你好,false
默认参数的本质特性
1. 参数默认值表达式可以引用前面的参数
function createPrice(price, tax = price * 0.1, total = price + tax) {
return { price, tax, total };
}
console.log(createPrice(100)); // { price: 100, tax: 10, total: 110 }
2. 默认值表达式是惰性求值的,每次函数调用都会重新计算
function genId(prefix = "", timestamp = Date.now()) {
return `${prefix}_${timestamp}`;
}
genId("user"); // 每次调用都会获取新的时间戳
3. 默认参数会影响函数的 length 属性
Function.length 返回的是函数“期望”的形参个数,默认为第一个带默认值的参数之前的位置。
function f1(a, b, c = 1) {}
console.log(f1.length); // 2
function f2(a = 1, b, c) {}
console.log(f2.length); // 0 —— 所有有默认值的参数及其后面的参数都不计算在内
实际场景中的最佳实践
- 用默认参数替代
||运算符,避免假值误判。 - 在需要惰性赋值(如当前时间)或依赖前一个参数的场景使用表达式。
- 可选参数尽量放在最后,保持参数的逻辑顺序。
9.2.2 剩余参数(Rest Parameters)
当函数需要处理可变数量的实参时,ES6 提供了 ... 剩余参数语法,将所有剩余实参收集到一个真正的数组中。
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
与 arguments 的关键区别
- 剩余参数是一个真正的数组,可以直接使用
map、filter、reduce等方法,不需要先转换。 - 剩余参数只包含那些没有对应形参的实参,而
arguments包含所有传入的实参。 - 剩余参数不影响箭头函数的
this,因为箭头函数内没有自己的arguments(它会捕获外层的arguments)。
混合使用与解构
剩余参数可以和普通参数混用,但必须是最后一个形参:
function show(prefix, separator, ...words) {
console.log(`前缀: ${prefix}`);
console.log(`分隔符: ${separator}`);
console.log(`单词列表:`, words);
}
show(">>", "-", "hello", "world");
// 前缀: >>
// 分隔符: -
// 单词列表: [ 'hello', 'world' ]
结合解构,还可以对剩余参数进行处理:
function join(first, ...others) {
console.log(first); // 第一个实参
console.log(others); // 剩余实参组成的数组
}
实际应用场景
- 替代
arguments:需要变参处理时,优先使用剩余参数。 - 在回调或高阶函数中转发参数:
function wrapper(fn, ...args) {
console.log('调用前');
const result = fn(...args);
console.log('调用后');
return result;
}
利用扩展运算符 ...args 展开数组传递给目标函数,这正是“剩余”与“展开”这对操作符的完美配合。
9.2.3 arguments 对象
arguments 是函数内部可以访问的一个类数组对象,保存着调用时传入的所有实参,与形参是否定义无关。
function demo(a, b) {
console.log(arguments[0], arguments[1]); // 传入的前两个实参
console.log(arguments.length); // 实参个数
console.log(arguments.callee); // 当前函数引用(严格模式禁用)
}
demo(1, 2, 3);
arguments 的特性与缺陷
- 它是一个类数组,但不是真实数组,不能直接使用数组方法。需要转换:
const args = Array.prototype.slice.call(arguments);
// 或
const args = Array.from(arguments);
// 或使用剩余参数
- 在非严格模式下,
arguments与形参会建立动态绑定:
function foo(x) {
arguments[0] = 99;
console.log(x); // 99 —— 修改 arguments 会影响 x
}
foo(10);
严格模式下不会建立这种绑定,两者独立。
- 箭头函数没有自己的
arguments,如果尝试使用,它会沿作用域链找到外层函数的arguments:
const arrow = () => {
console.log(arguments); // 这里的 arguments 是外层函数的(如果在全局则是报错或空)
};
- 在 ES6 默认参数的作用域中,
arguments的行为会略有不同,默认值不会反映到arguments中。
arguments 的现存价值
在现代 JavaScript 中,剩余参数几乎总能替代 arguments 且更加清晰,因此推荐优先使用剩余参数。arguments 主要在以下场景仍有存在:
- 维护老旧代码时需要理解。
- 某些动态调用场景(如不确定参数传递)偶尔可见,但都可以通过剩余参数和扩展运算符实现。
// 旧风格
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// 新风格(更推荐)
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
9.2.4 综合对比与选择指南
| 特性 | 默认参数 | 剩余参数 | arguments 对象 |
|------------------|---------------------------------|---------------------------------|----------------------------------|
| 本质 | 为形参提供默认值 | 将多余实参收集为数组 | 类数组,包含所有实参 |
| 触发条件 | 实参为 undefined | 总是收集 | 函数调用时自动创建 |
| 能否用数组方法 | 不适用(它是单个参数) | 直接可用 | 需先转换为数组 |
| 箭头函数支持 | 支持 | 支持 | 不支持(会捕获外层) |
| 与形参的关系 | 一一对应 | 最后一个形参,收集剩余 | 与形参有动态绑定(非严格模式) |
| 推荐度 | 优先使用 | 优先使用 | 了解即可,新代码中避免使用 |
编缉规则:写新代码时,用默认参数提供哨兵值,用剩余参数处理变参,忘掉 arguments 的存在。这会让你的函数签名更清晰、更可读、更不易出错。