完全理解 JS:arguments
Category(分类): JavaScript Status: 已整理
什么是 arguments?
调用非箭头函数时,函数内部通常可以访问一个本地的 arguments 对象。它记录本次调用实际传入的参数数量和参数值:
arguments.length是实际传入的实参数量,不是函数声明的形参数量;arguments[0]、arguments[1]等按索引读取实参;- 它是 array-like(类数组对象),不是
Array实例; - 箭头函数没有自己的
arguments,箭头中的arguments会词法捕获外层非箭头函数的绑定; - 类方法和构造器有自己的
arguments,但类体是严格模式,参数对象不会与形参映射。
调用处提供的是实参(actual arguments),函数声明中的变量是形参(formal parameters)。JavaScript 按值传递;当值是对象时,传递的是对象引用这一种值,不是按引用传递。
function inspect(first, second) {
console.log(arguments.length)
console.log(arguments[0], arguments[1], arguments[2])
console.log(first, second)
}
inspect('a', 'b', 'extra')
inspect('only one')
arguments 没有 Array.prototype 上的 push 等方法:
function checkArguments() {
console.log([] instanceof Array) // true
console.log(arguments instanceof Array) // false
console.log(Array.isArray(arguments)) // false
console.log(arguments.push) // undefined
// 可以借用数组方法,但现代代码通常更推荐 rest 参数。
Array.prototype.push.call(arguments, 'added')
console.log(arguments[arguments.length - 1]) // added
}
checkArguments(1, 2)
“使用 arguments.push 会报错”并不准确:上面的属性通常只是 undefined;只有直接调用 arguments.push('x') 才会因为不是函数而抛 TypeError。
创建一个灵活的格式化函数
原文使用 %1、%2 这样的占位符。动态构造字符类的写法无法正确处理 %10,还会把模板字符串本身误算进 arguments.length。可以使用 rest 参数和多位数字匹配:
function format(template, ...values) {
return String(template).replace(/%(\d+)/g, (match, digits) => {
const index = Number(digits) - 1
return index >= 0 && index < values.length
? String(values[index])
: match
})
}
console.log(
format(
'And the %1 want to know whose %2 you %3',
'papers',
'shirt',
'wear'
)
)
// And the papers want to know whose shirt you wear
console.log(format('value=%10', ...Array.from({ length: 10 }, (_, i) => i)))
这里的 format 是位置格式化函数,不是 ECMAScript 的模板字面量。模板字面量是另一种语法,例如 `hello ${name}`。
如果必须兼容旧代码,也可以直接使用 arguments:
function formatWithArguments(template) {
const values = arguments
return String(template).replace(/%(\d+)/g, (match, digits) => {
const index = Number(digits)
return index >= 1 && index < values.length
? String(values[index])
: match
})
}
console.log(formatWithArguments('I like %1, not %2.', 'JavaScript', 'Java'))
现代可变参数函数优先使用 ...rest,因为 rest 参数直接是真正的数组;需要了解旧 API 或参数映射时再使用 arguments。
参数对象与形参的映射
arguments 与形参是否联动取决于函数形式。只有在**非严格模式 + simple parameter list(没有默认值、rest 或解构)**时,普通函数通常具有 mapped arguments:
// 需要在浏览器 classic script 的非严格环境中观察 mapped 行为。
function mapped(a) {
a = 2
return arguments[0]
}
function mappedBack(a) {
arguments[0] = 3
return a
}
console.log(mapped(1)) // 2
console.log(mappedBack(1)) // 3
严格函数、模块、类方法以及使用默认参数/rest/解构参数的函数使用 unmapped arguments:
function strictArguments(a) {
'use strict'
a = 2
return arguments[0]
}
function withDefault(a = 1) {
a = 2
return arguments[0]
}
function withRest(first, ...rest) {
return [arguments[0], first, rest]
}
console.log(strictArguments(1)) // 1
console.log(withDefault(1)) // 1
console.log(withRest(1, 2, 3)) // [1, 1, [2, 3]]
如果参数列表含默认值、rest 或解构,函数体内不能再通过单独的函数级 'use strict' 指令切换严格模式;严格模式应由外层 classic script、模块或类提供。
把 arguments 转换成真正的数组
历史写法是借用 slice:
function toArrayLegacy() {
return Array.prototype.slice.call(arguments)
}
console.log(toArrayLegacy(1, 2, 3)) // [1, 2, 3]
现代写法通常更清晰:
function toArrayModern() {
return Array.from(arguments)
}
function toArrayWithRest(...items) {
return items
}
function toArrayWithSpread() {
return [...arguments]
}
console.log(toArrayModern('a', 'b'))
console.log(toArrayWithRest('a', 'b'))
console.log(toArrayWithSpread('a', 'b'))
Array.from 和展开语法会创建新数组;它们不会让调用者传入的对象自动变成数组。类数组对象至少需要合理的 length 和索引才能得到预期结果。
跨 realm 时,另一个 iframe 中创建的真数组可能使 value instanceof Array 为 false;检测真数组应使用 Array.isArray(value)。arguments 本身仍然不是数组。
通过参数对象封装函数
原文的 makeFunc 展示了“预置参数 + 后续参数”的闭包。apply 的第一个参数是 thisArg,不是函数的 lexical scope;arguments 也不是 apply 的参数数组。
function makeFunc(func, ...preset) {
if (typeof func !== 'function') {
throw new TypeError('func must be callable')
}
return function wrapped(...later) {
return Reflect.apply(func, this, [...preset, ...later])
}
}
const formatMessage = (template, first, second) =>
format(template, first, second)
const message = makeFunc(formatMessage, 'I like %1 not %2.')
console.log(message('JavaScript', 'Java'))
console.log(message('TypeScript', 'JavaScript'))
这个 helper 会保留调用 wrapped 时的 this,但它仍不能构造 class;如果目标是构造调用,应使用 new 或 Reflect.construct,不能使用 apply。
创建引用自身的函数
原文使用 arguments.callee 让匿名函数引用自身。arguments.callee 是遗留 API:严格模式、模块、类方法和非简单参数中访问它会抛错,也会妨碍引擎优化。现代代码使用命名函数表达式或命名函数:
function repeat(fn, times, delay) {
if (typeof fn !== 'function') {
throw new TypeError('fn must be callable')
}
if (!Number.isInteger(times) || times < 0) {
throw new RangeError('times must be a non-negative integer')
}
return function run(...args) {
if (times-- <= 0) return
Reflect.apply(fn, this, args)
if (times > 0) {
const receiver = this
setTimeout(() => Reflect.apply(run, receiver, args), delay)
}
}
}
const say = repeat((value) => console.log(value), 3, 2_000)
say('Can you hear me, major tom?')
这个示例第一次调用立即执行,后续调用由宿主定时器调度;2_000 是最小延迟提示,不保证每隔精确两秒执行。生产实现还应考虑取消、页面销毁、异常传播和 delay 的有效范围。
历史代码中可能见到:
// 仅作历史识别,不要在严格模式或模块中使用。
function legacyRecursive() {
return arguments.callee()
}
不要把 arguments.callee 当作“惊喜功能”。命名函数表达式既能自引用,也不会依赖被弃用的调用参数对象属性。
arguments、rest 与函数长度的区别
function demo(a, b = 1, ...rest) {
return {
actualCount: arguments.length,
declaredCount: demo.length,
first: a,
rest
}
}
console.log(demo(10, 20, 30, 40))
// { actualCount: 4, declaredCount: 1, first: 10, rest: [30, 40] }
arguments.length:这一次调用实际传入的参数数量;demo.length:函数定义中第一个默认参数之前的形参数量;rest:从指定位置开始收集的真正数组;arguments:非箭头函数的类数组参数对象,主要用于兼容旧 API 或解释参数映射。
小结
arguments通常只属于非箭头函数的单次调用,不是数组,也不是每个函数对象都拥有的全局属性。- JavaScript 的实参值被赋给形参;对象传递的是引用值的副本,不是按引用传递。
- 非严格 simple 参数函数可能存在 mapped
arguments;严格、模块、类和非简单参数使用 unmapped 语义。 - 现代可变参数函数优先使用 rest 参数,转换类数组可使用
Array.from或展开语法。 arguments.callee已是遗留 API,使用命名函数表达式替代。