为什么 JavaScript 函数的 arguments 参数是类数组而不是数组?如何遍历类数组?
为什么 arguments 是类数组而不是数组根本原因历史设计与性能考量1. 历史遗留 —arguments比 Array 更早JavaScript 1.0 时代就有了arguments当时 Array 原型上方法很少只有join/reverse/sort设计者选择用一个轻量普通对象来承载实参信息而非创建完整数组实例// arguments 的本质结构functionfoo(a,b,c){// arguments 内部大致等价于// { 0: a的值, 1: b的值, 2: c的值, length: 实参数量, callee: foo }}创建普通对象比创建数组实例开销更小在早期 JS 引擎中差异显著。2. 性能 — 避免数组实例化的完整代价真正的数组需要继承Array.prototype整个原型链维护内部[[Class]]标识具备动态 length 自动更新机制arguments只是一个结构简单的普通对象每次函数调用时只需写入索引属性和 length不触发任何数组特有逻辑。在高频调用的函数中这种轻量化设计能减少 GC 压力。3. 特有属性 —calleefunctionfoo(){console.log(arguments.calleefoo);// true}arguments.callee指向函数自身这是普通对象的属性数组没有这个概念。后来callee在严格模式下被禁用影响引擎优化但这个设计决策已经固化了arguments的对象身份。4. 引擎优化 — arguments 的特殊地位V8 等引擎对arguments做了深层优化普通调用时arguments 不真正创建对象仅在访问时按需 materialize 从未被引用时完全零开销连对象都不分配如果arguments是数组每次调用都必须创建数组实例优化空间会小得多。这也解释了为什么 ES6 引入rest 参数后推荐替代arguments——rest 参数直接创建真数组但引擎可以针对这种明确语义做更精准的优化。5. 与具名参数的联动functionfoo(a){arguments[0]100;console.log(a);// 100非严格模式下arguments 与具名参数双向绑定}foo(1);arguments与具名参数之间存在双向映射关系这是普通对象的特殊行为。如果用数组实现就需要额外的同步机制来维持这种映射增加复杂度。严格模式下切断了这种绑定进一步印证了这是一个历史包袱。如何遍历类数组方法一转数组后遍历推荐functionfoo(){// Array.from — 最通用Array.from(arguments).forEach(argconsole.log(arg));// 展开运算符 — 最简洁[...arguments].forEach(argconsole.log(arg));// slice.call — ES5 兼容Array.prototype.slice.call(arguments).forEach(argconsole.log(arg));}方法二直接用 for 循环functionfoo(){for(leti0;iarguments.length;i){console.log(arguments[i]);}}最原始性能最好适用于所有类数组。方法三借用数组方法functionfoo(){Array.prototype.forEach.call(arguments,arg{console.log(arg);});// 或用 call 的简写[].forEach.call(arguments,argconsole.log(arg));}不创建新数组直接在类数组上调用数组原型方法。方法四for…of需可迭代functionfoo(){// arguments 天然可迭代可直接 for...offor(constargofarguments){console.log(arg);}}arguments和NodeList内置了[Symbol.iterator]可以直接用。但自定义类数组对象不行constlike{0:a,1:b,length:2};for(constxoflike){}// TypeError: like is not iterable// 需手动添加迭代器或先转换for(constxofArray.from(like)){console.log(x);// a, b}方法五rest 参数 — 从源头避免// ES6 最佳实践用 rest 参数替代 argumentsfunctionfoo(...args){// args 就是真数组直接使用所有数组方法args.forEach(argconsole.log(arg));args.map(argarg*2);args.filter(argarg0);}对比总结遍历方式需转换兼容性适用范围for循环否全部所有类数组Array.from() 遍历是ES6所有类数组[...] 遍历是ES6可迭代类数组借用Array.prototype否ES5所有类数组for...of否ES6可迭代类数组rest 参数...args否ES6从源头替代 arguments一句话arguments是类数组源于历史设计性能考量实际开发中优先用 rest 参数...args直接拿到真数组需要遍历时Array.from()最通用for...of最简洁。