先记住这个答案
函数调用时会基于实际传入的参数创建 arguments 对象;parameters 是函数定义中声明的形参,例如 function f(a, b) 中的 a、b,是函数内的局部绑定。arguments 具有数字索引和 length,但没有数组方法。在非严格且参数只由简单名字组成的函数里,修改 arguments[0] 会同步改变对应形参,反之亦然;严格模式或存在默认参数、剩余参数、解构时,这种同步消失。
arguments保存实际传入值,是类数组对象parameters是声明的形参,函数体内是局部绑定- 严格模式或默认参数下二者不再同步
形参绑定与 arguments 的生成规则
形参是函数定义中的局部绑定,例如 function search(id) 中的 id。普通函数通常提供一个记录本次实参的 arguments 对象。调用 search(1, 2, 3) 时,其 arguments.length 为 3,而 search.length 为 1。函数的 length 统计第一个带默认值的形参之前的参数个数,不计剩余参数;一个解构参数按一个计数,不能把它一概叫作全部形参数量。
arguments 被叫作 array-like object:它有 length 属性和索引,也实现了 Symbol.iterator,但它不继承数组方法。arguments.map 并不存在,想遍历可以先 Array.from(arguments) 转成数组。typeof arguments 的结果是 "object",用 Array.isArray(arguments) 会得到 false。
非严格模式且参数列表只有简单名字时,实际传入的实参项可与对应形参双向关联。严格模式、默认参数、剩余参数或解构参数会关闭这种映射。即使满足映射条件,也不能假设没有传入的参数项会凭空建立关联;删除关联的索引属性等操作还会解除对应映射。工程代码应显式使用参数,不依赖这种历史行为。
function loose(value) { arguments[0] = 7; return value; }
function strict(value) { 'use strict'; arguments[0] = 7; return value; }
function defaults(value = 1) { arguments[0] = 7; return value; }
console.log(loose(2));
console.log(strict(2));
console.log(defaults(2));
console.log(loose());
function counts(a, b = 0, ...rest) { return arguments.length; }
console.log(counts.length, counts(1, 2, 3));查看输出与解释
7
2
2
undefined
1 3按普通脚本运行,第一项保留非严格模式。简单参数的已传入实参项可以同步;严格模式和默认参数不映射。未传入参数时,补写 arguments[0] 不会补建映射。最后一行对比函数声明的 length 与本次实参数量。
可变参数数量但只声明一个形参的场景
场景:函数 prefixJoin(prefix) 只声明一个形参,调用方却会传多个字符串:prefixJoin(">", "A", "B", "C")。由于 arguments 记录全部实参,函数体内先 Array.prototype.slice.call(arguments, 1) 把第二个及之后的实参复制成数组,再使用 join(prefix),最终返回 "A>B>C"。这里声明形参数量是 1,实际实参数量是 4,二者不一致正是 arguments 发挥作用的地方。
这个方案必须写在普通函数里,因为箭头函数没有自己的 arguments(若改用箭头函数,需要换成剩余参数写法);也不能事先列出所有形参,因为调用方数量不定。选择先进行数组转换,是因为 arguments 本身没有 join 或 map,直接当数组使用会得到 TypeError。转换后所有后续处理都基于普通数组,也不再受到 arguments 与形参同步语义的影响。
什么条件下不能依赖 arguments 与形参同步
最容易失效的写法是在函数体内用 arguments[0] = 999 去修改第一个形参。在无默认参数的旧式非严格函数中它确实有效;但函数中写了严格模式指令,或声明形参时带 a = 1 默认值,修改就不会同步到形参。反过来,给形参重新赋值后读取 arguments[0] 仍会得到调用时传入的旧值,因此不能盲目依赖二者互相更新。
箭头函数没有自己的 arguments 绑定,引用这个名字时会查找外层作用域;外层也没有相应绑定时会报错。箭头函数需要当前调用的全部实参时,应写剩余参数,例如 (...values) => values.length。剩余参数直接得到数组,可变参数 API 通常优先采用这种更明确的表达。
容易答错的地方
- 把 arguments 当作真正的数组
- 它确实有
length和下标,但原型链上并没有Array.prototype,因此arguments.forEach是 undefined,不能直接调用。可以用Array.from(arguments)、[...arguments]或Array.prototype.slice.call(arguments)转成真数组。 - 认为严格模式下不能使用 arguments
- 严格模式仍然允许使用
arguments,只是arguments与形参不再双向同步,同时不能访问arguments.callee。若函数的形参里包含默认值、剩余参数或解构参数,函数体内也不能再写"use strict";指令,这是语法限制,会抛出语法错误。
面试官还会怎么问?
如何把 arguments 转换为真正的数组?
可以使用 Array.from(arguments)、[...arguments] 或 Array.prototype.slice.call(arguments)。后者在老环境兼容性更好。转换结果都是普通数组,之后可以安全调用数组方法,也不会再受 arguments 与形参之间的同步关系影响。
默认参数会出现在 arguments 中吗?
默认参数不会填入 arguments。比如 function f(a = 1){} 调用 f() 时,arguments.length 是 0,arguments[0] 是 undefined;调用 f(5) 时 arguments[0] 才是 5。同时在这种函数中,arguments 与 a 的更新不联动。
arguments.callee 被禁后如何获得当前函数自身?
应使用具名函数表达式或在作用域内持有函数名引用,例如 const f = function myself(){ console.log(myself); };。严格模式禁止访问 arguments.callee,因为通过它引用当前执行函数会让内联等优化难以进行,而且具名函数表达式已经能覆盖函数自引用(如递归)的需求。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。