先记住这个答案
类数组对象是指有 length 属性、并且用 0、1、2 这样的数字作为键的对象。它长得像数组,但原型不是 Array.prototype,所以没有 map、forEach、push 等数组方法。arguments 是函数内部自动提供的类数组,装着调用时传入的所有参数,它的原型是 Object.prototype,所以 Array.isArray(arguments) 是 false。要转成真数组,可以用 Array.from(arguments)、[...arguments],老代码里常见 Array.prototype.slice.call(arguments)。ES2015 之后,更推荐直接用剩余参数 function fn(...args),这里的 args 本身就是真数组。
- 类数组有
length和下标,但没有数组方法 arguments的原型是Object.prototype- 转换用
Array.from、展开或slice.call - 箭头函数没有自己的
arguments,用剩余参数代替
在函数里看看 arguments 是什么
在函数里把 arguments 的几个特征打印出来,就能看清它和数组的区别。
function demo() {
console.log(arguments.length, arguments[0])
console.log(Array.isArray(arguments), typeof arguments.map) // 不是数组,没有 map
console.log(Object.prototype.toString.call(arguments))
const list = Array.from(arguments) // 转成真数组
console.log(Array.isArray(list), list.map((s) => s.toUpperCase()).join(','))
}
demo('a', 'b')查看输出与解释
2 a
false undefined
[object Arguments]
true A,Barguments 像一张打印出来的名单,能按序号查人,但没有表格软件那些排序、筛选功能。转成数组后,这些功能就都有了。
三种转换写法的区别在于对输入的要求。[...arguments] 依赖迭代器,arguments 在现代引擎里是可迭代的,没问题;但对于自己拼出来的 { 0: 'a', length: 1 },展开会报错。Array.from 两种都支持。Array.prototype.slice.call(obj) 只看 length 和下标,是 ES5 时代的通用写法。
非严格模式下的形参联动
arguments 还有一个数组没有的行为:在非严格模式下,如果函数只用了简单形参,arguments[0] 和第一个形参是绑在一起的,改一个另一个也跟着变。
function sloppy(a) {
arguments[0] = 'changed' // 非严格模式:a 也被改了
return a
}
function strict(a) {
'use strict'
arguments[0] = 'changed' // 严格模式:a 不受影响
return a
}
console.log(sloppy('orig'), strict('orig'))查看输出与解释
changed orig这种联动很容易造成难以理解的 bug,所以严格模式取消了它。另外只要函数用了默认参数、剩余参数或者解构参数,即使在非严格模式下,联动也会被关闭。ES 模块和 class 内部默认就是严格模式。
- 剩余参数才是新代码的写法
function sum(...nums) { return nums.reduce((a, b) => a + b, 0) }里的nums是真数组,直接能用reduce,不需要任何转换。它还能和普通形参一起用,比如function log(level, ...messages),只收集剩下的参数,而arguments永远包含全部参数。
容易答错的地方
- 在箭头函数里用
arguments - 箭头函数没有自己的
arguments,在里面访问到的是外层普通函数的arguments;如果外层没有函数,就会报ReferenceError,在 Node.js 的 CommonJS 模块里则会拿到模块包装函数的参数。箭头函数需要所有参数时,用(...args) => {}。 - 以为
arguments.length是形参个数 arguments.length是这次调用实际传了几个参数。function f(a, b) {}调用f(1)时,arguments.length是 1。声明的形参个数要用f.length读,它是 2。f.length只数第一个默认参数或剩余参数之前的形参。- 以为所有类数组都能直接展开
arguments、NodeList、字符串都实现了迭代器,可以展开;但普通的{ length: 2, 0: 'a', 1: 'b' }没有迭代器,[...obj]会抛TypeError。不确定时用Array.from。
面试官还会怎么问?
除了 arguments,还有哪些常见的类数组?
DOM 查询结果 NodeList 和 HTMLCollection、字符串、TypedArray,以及 jQuery 对象。HTMLCollection 是实时的,比如 document.getElementsByTagName('div') 返回的集合,页面新增一个 div,它的 length 立刻就变了,边遍历边删除时要特别小心。
不转换,能直接对 arguments 调用数组方法吗?
可以借用。Array.prototype.map.call(arguments, fn)、Array.prototype.join.call(arguments, ',') 都能用,因为大多数数组方法只依赖 length 和下标,不要求 this 是真数组。
arguments.callee 还能用吗?
严格模式下访问会抛 TypeError。它以前用来在匿名函数里递归调用自己,现在应该给函数表达式起个名字,比如 const f = function fact(n) { return n <= 1 ? 1 : n * fact(n - 1) }。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。