8.5JavaScript函数 arguments
一、arguments
详解
1. 什么是 arguments
?
arguments
是一个类数组对象(array-like object),存在于非箭头函数的函数作用域中。- 它包含了调用函数时传入的所有实参,即使函数定义时没有声明形参。
- 它具有
length
属性,可以通过索引访问(如arguments[0]
),但不具备数组的方法(如.map()
、.forEach()
等)。
2. 基本用法示例
function foo(a, b) {console.log(arguments.length); // 实参个数console.log(arguments[0]); // 第一个实参console.log(arguments[1]); // 第二个实参console.log(arguments[2]); // 第三个实参(即使没定义形参)
}foo(1, 2, 3);
// 输出: 3, 1, 2, 3
3. arguments
的特性
与形参绑定(在非严格模式下):
function test(a) {console.log(a, arguments[0]); // 1, 1a = 2;console.log(a, arguments[0]); // 2, 2(非严格模式下会同步) } test(1);
⚠️ 在 严格模式('use strict') 下,这种绑定被解除,修改
a
不会影响arguments[0]
。不是真正的数组:
function foo() {console.log(Array.isArray(arguments)); // false// arguments.map(x => x * 2); // 报错! }
箭头函数中没有
arguments
:const arrow = () => {console.log(arguments); // ReferenceError: arguments is not defined };
二、严格模式下 arguments
仍然存在
'use strict';function foo(a, b) {console.log(arguments[0]); // 正常访问console.log(arguments.length); // 正常
}foo(1, 2); // 输出: 1, 2
✅ 结论:
arguments
在严格模式下依然可用,不是被禁用的对象。
三、严格模式下 arguments
的关键变化
1. 与形参不再“绑定”(解耦)
这是最重要的区别!
非严格模式(怪异行为):
function f(a) {a = 10;console.log(arguments[0]); // 10(同步修改!)
}
f(5);
严格模式(行为正常):
'use strict';
function f(a) {a = 10;console.log(arguments[0]); // 5(不再同步!)
}
f(5);
✅ 优点:避免了令人困惑的隐式副作用,使代码更可预测。
2. arguments.callee
和 arguments.caller
被禁用
'use strict';
function f() {console.log(arguments.callee); // ❌ TypeError!
}
这两个属性在严格模式下访问会直接抛出错误。
但 arguments
本身的其他功能(如 arguments[0]
, arguments.length
)完全正常。
3. arguments
仍然是类数组对象
- 依然不是真正的数组(即使在严格模式下)。
- 仍然需要转换才能使用数组方法:
'use strict'; function f() {const args = Array.from(arguments);// 或 [...arguments](但注意:严格模式下不能在箭头函数中用 arguments) }
四
、arguments.callee
?
arguments.callee
指向当前正在执行的函数对象。- 它存在于
arguments
对象上(仅限非箭头函数)。
示例(传统用法):
// 匿名函数递归(ES3/ES5 常见写法)
var factorial = function(n) {if (n <= 1) return 1;return n * arguments.callee(n - 1); // 调用自身
};console.log(factorial(5)); // 120
在这个例子中,因为函数是匿名的,无法通过名字调用自己,所以用 arguments.callee
实现递归。
现在替代方案 :使用命名函数表达式
var factorial = function fact(n) {if (n <= 1) return 1;return n * fact(n - 1); // 通过内部名字调用
};console.log(factorial(5)); // 120
即使外部变量名被修改,
fact
在函数内部依然有效。
五
、arguments.caller
arguments.caller
是arguments
对象上的一个属性。- 理论上,它应返回调用当前函数的那个函数(即“调用者”)。
- 仅存在于非严格模式的非箭头函数中。
示例:
function callerFunc() {calleeFunc();
}function calleeFunc() {console.log(arguments.caller); // 期望输出 callerFunc
}callerFunc();
但在实际中,这个属性的行为极不稳定,且在多数现代引擎中已被移除或设为 null
/undefined
。
除了 arguments.caller
,还有一个类似的属性:Function.caller
(函数对象自身的属性)。
function f() {console.log(f.caller); // 调用 f 的函数
}
- 同样被废弃。
- 同样在严格模式下访问会报错。
- 行为同样不可靠。
如果确实需要知道“谁调用了我”,应通过参数显式传递:
function handleAction(callerName, data) {console.log(`${callerName} triggered action with`, data);
}function buttonClick() {handleAction('buttonClick', { id: 1 });
}
六、函数对象的 .length
属性
基本用法
function foo(a, b, c) {// ...
}console.log(foo.length); // 输出: 3
⚠️ 注意:
.length
返回的是函数声明中命名参数的个数,不包括:
- 剩余参数(
...args
)- 默认参数(从第一个有默认值的参数开始,后面的参数不计入)
- 解构参数(某些情况下可能为 0)
📌 规则详解(ES6+)
根据 ECMAScript 规范,函数的 .length
是这样确定的:
1. 普通参数
function f(a, b) {}
console.log(f.length); // 2
2. 剩余参数(Rest Parameters)不计入
function f(a, b, ...rest) {}
console.log(f.length); // 2(...rest 不算)
3. 默认参数:从第一个带默认值的参数开始,之后的都不计入
function f(a, b = 1, c) {}
console.log(f.length); // 1(b 有默认值,b 和 c 都不算)function f(a, b, c = 1) {}
console.log(f.length); // 2(只有 c 有默认值,所以 a、b 计入)function f(a = 1, b, c) {}
console.log(f.length); // 0(a 有默认值,后面全部不算)
4. 解构参数:通常 .length
为 0
function f({ name, age }) {}
console.log(f.length); // 0function f([x, y]) {}
console.log(f.length); // 0
即使解构中有默认值,也一样:
function f({ name = 'Alice' } = {}) {}
console.log(f.length); // 0
5. 箭头函数同样适用
const f = (a, b, c) => {};
console.log(f.length); // 3const g = (a, b = 1) => {};
console.log(g.length); // 1
6. 类构造函数
class MyClass {constructor(a, b) {}
}
console.log(MyClass.length); // 2
❌ 不能获取“实际调用时传入的参数个数”
函数的 .length
是静态的,只反映声明时的形参设计,不是运行时传入的实参个数。
要获取调用时实际传入的参数个数,需在函数内部使用:
arguments.length
(非箭头函数)- 或通过 剩余参数
...args
的长度
function f() {console.log('声明参数个数:', f.length); // 0console.log('实际传入个数:', arguments.length); // 运行时决定
}f(1, 2, 3);
// 输出:
// 声明参数个数: 0
// 实际传入个数: 3
箭头函数没有
arguments
,只能用...args
:
const f = (...args) => {console.log('实际传入个数:', args.length);
};
七、更现代的剩余参数
ES6 的设计哲学之一是让常见操作更简洁、更符合直觉。剩余参数做到了:
✅ 显式声明可变参数
function log(first, ...rest) {console.log('First:', first);console.log('Others:', rest); // rest 是真正的数组
}
- 从函数签名即可看出参数结构。
- 代码自文档化(self-documenting)。
✅ 原生支持数组方法
function sum(...numbers) {return numbers.reduce((a, b) => a + b, 0); // 直接调用 reduce
}
✅ 与解构赋值风格统一
剩余参数语法 ...
与**扩展运算符(Spread Operator)**共享相同符号,形成对称设计:
- Rest(收集):
function f(...args)
→ 把多个参数“收集”成数组。 - Spread(展开):
f(...array)
→ 把数组“展开”为参数。
这种对称性提升了语言的一致性和表达力。
提升工具链和类型系统的友好性
- 静态分析更简单:工具(如 ESLint、Babel、TypeScript)能更容易理解
...args
的意图。 - TypeScript 类型推断更准确:
function f(...args: number[]) { /* args 类型明确 */ }
arguments
在 TS 中类型复杂且不直观。
推动函数式编程风格
剩余参数天然支持高阶函数和无参数数量限制的组合函数:
const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value);
这种写法在函数式编程中非常常见,而 arguments
会让代码变得笨重。
与模块化和现代 API 设计兼容
现代 JavaScript 强调明确性和可预测性。剩余参数让函数接口更清晰,便于:
- 编写文档
- 单元测试(明确知道哪些是固定参数,哪些是可变参数)
- 构建灵活的 API(如
console.log(...items)
)
总结:ES6 引入剩余参数的核心动机
问题(ES5 时代) | ES6 解决方案 |
---|---|
arguments 不是数组 | ...args 是真正的数组 |
语法隐式、不直观 | 显式声明,语义清晰 |
与形参绑定行为怪异 | 无绑定,行为可预测 |
不支持箭头函数 | 全函数类型通用 |
与语言其他特性割裂 | 与扩展运算符、解构等统一设计 |
💡 剩余参数不是为了“替代”
arguments
而存在,而是为了解决 JavaScript 在可变参数处理上的设计缺陷,使语言更简洁、安全、一致。
如今,在新代码中应始终优先使用 ...args
,仅在维护旧代码或特殊场景下才考虑 arguments
。