在日常开发中,你经常会遇到一些长得像数组、但又不是真正数组的对象——它们有 length 属性,可以通过索引访问元素,却没有数组原生的那些强大方法(push、forEach、map 等)。这类对象统称为类数组对象(Array-like Object),理解它们并掌握转换技巧,能让你在处理 DOM 操作、函数参数等场景时游刃有余。
什么是类数组对象
类数组对象需要满足两个基本条件:
- 拥有
length属性,且值为非负整数。 - 拥有若干索引属性(
0、1、2……),范围在0 ≤ index < length。
除此之外,它的原型链上并没有 Array.prototype,因此无法直接调用数组方法。
典型的类数组对象包括:
- 函数的
arguments对象 - DOM 方法返回的节点列表(
NodeList),如document.querySelectorAll()的返回值 - 字符串(可用索引访问字符,有
length) - 手动构造的普通对象,如
{ 0: 'a', 1: 'b', length: 2 }
function demo() {
console.log(arguments); // Arguments(3) [1, 2, 3]
console.log(arguments.length); // 3
console.log(arguments[0]); // 1
// arguments.push(4); // TypeError: arguments.push is not a function
}
demo(1, 2, 3);
const divs = document.querySelectorAll('div');
console.log(divs.length); // 页面上的 div 数量
console.log(divs[0]); // 第一个 div 元素
// divs.map(div => div.textContent); // TypeError: divs.map is not a function
字符串也算一种特殊的类数组:
const str = 'hello';
console.log(str.length); // 5
console.log(str[1]); // 'e'
为什么要转为真正的数组
转为真正的数组后,你就能使用 map、filter、reduce、forEach、find 等强大的数组方法,让数据处理变得简洁高效。这在以下场景尤为常见:
- 对函数参数
arguments进行遍历或转换(虽然现代开发中更推荐使用剩余参数...args) - 对
NodeList进行批量操作,比如过滤出包含特定类的元素 - 对字符串进行类似数组的处理(虽然直接用
split('')也能转为数组)
转换方法一:Array.from()(最推荐)
ES6 引入了 Array.from(),能将类数组对象或可迭代对象直接转为数组,语义清晰,代码简洁,是目前最推荐的方式。
// 转换 arguments
function sum() {
const args = Array.from(arguments);
return args.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// 转换 NodeList
const paragraphs = document.querySelectorAll('p');
const texts = Array.from(paragraphs, p => p.textContent);
// 第二个参数是可选的映射函数,相当于 .map()
// 转换字符串
const chars = Array.from('JavaScript');
console.log(chars); // ['J','a','v','a','S','c','r','i','p','t']
Array.from() 还能处理有 length 属性的任意对象,即使它没有索引值:
const fakeArray = { length: 3 };
console.log(Array.from(fakeArray)); // [undefined, undefined, undefined]
转换方法二:扩展运算符 ...(针对可迭代对象)
扩展运算符只能转换可迭代对象(即实现了 Symbol.iterator 方法的对象)。arguments 和 NodeList 都是可迭代的,但手动构造的纯对象(如 {0:'a',1:'b',length:2})不是可迭代对象,不能用扩展运算符。
// arguments 可迭代
function demo() {
const args = [...arguments];
console.log(args); // [1, 2, 3]
}
demo(1, 2, 3);
// NodeList 可迭代
const spans = document.querySelectorAll('span');
const spanArray = [...spans];
// 字符串可迭代
console.log([...'hi']); // ['h', 'i']
// 不可迭代的类数组会报错
const obj = { 0: 'a', 1: 'b', length: 2 };
// [...obj]; // TypeError: obj is not iterable
判断一个对象是否可迭代,可以检查是否存在 Symbol.iterator:
typeof arguments[Symbol.iterator] === 'function'; // true
typeof document.querySelectorAll('div')[Symbol.iterator] === 'function'; // true
转换方法三:Array.prototype.slice.call()(传统方法)
在 ES6 之前,这是最常用的转换手段。它利用了 slice 方法在复制数组时,会遍历 length 和索引元素的特性。
function demo() {
const args = Array.prototype.slice.call(arguments);
// 或者用简写:const args = [].slice.call(arguments);
console.log(args); // [1, 2, 3]
}
demo(1, 2, 3);
const links = document.getElementsByTagName('a');
const linkArray = [].slice.call(links);
这种方法适用于任何类数组对象(只要有 length 和索引),不受可迭代限制。但代码稍显晦涩,现代开发中更推荐 Array.from()。
手动遍历转换(兼容性兜底)
如果你需要支持极古老的浏览器(如 IE8 以下),可以用一个简单的循环手动构建数组:
function toArray(arrayLike) {
const arr = [];
for (let i = 0; i < arrayLike.length; i++) {
arr.push(arrayLike[i]);
}
return arr;
}
除非有特殊兼容需求,否则不推荐手写,直接用 Array.from() 即可。
注意事项与最佳实践
Array.from()vs[...]
Array.from()更强大:可以处理不可迭代的类数组,第二个参数能直接映射。- 扩展运算符语法更短,但仅适用于可迭代对象。
- 对
arguments或者NodeList两者都可用,场景选择时优先考虑可读性和是否需要映射。
- 尽量不用
arguments
在 ES6+ 中,建议使用剩余参数 ...args 来替代 arguments,因为 ...args 直接得到一个真数组,无需转换,且箭头函数没有自己的 arguments 对象。
NodeList的实时性问题
使用 querySelectorAll 返回的 NodeList 是静态的(不会随 DOM 变化更新);但 getElementsByTagName 等返回的 HTMLCollection 是动态的。转换成数组后,两者都会变成固定快照,不用担心后续 DOM 变动影响已转换的数组。
- 字符串的特殊性
字符串既是类数组,又是可迭代对象。但需要注意,转换为数组后得到的是单个字符,对于包含表情符号或其他 Unicode 字符的字符串,Array.from 能正确处理代理对,而简单的 str.split('') 可能会出问题:
const emoji = '😀👍';
console.log(emoji.length); // 4(因为每个表情是两个 UTF-16 码元)
console.log(emoji.split('')); // ['\uD83D', '\uDE00', '\uD83D', '\uDC4D'] 乱码
console.log(Array.from(emoji)); // ['😀', '👍'] 正确
因此对字符串转换推荐使用 Array.from(str)。
掌握类数组对象的本质和转换方法,能让你的数组操作代码更加统一、安全、易读,避免一堆丑陋的 for 循环或报错。这是前端工程师必备的基础技能之一。