- 教程
【免费下载链接】jstips
This is about useful JS tips!
document.querySelectorAll()返回的是"类数组"(array-like)的 NodeList,它拥有索引与length,却缺少map、forEach、slice等数组方法,直接调用会抛错。本篇以 jstips 仓库的 第 8 期技巧 为骨架,系统讲解Array.apply、Array.prototype.slice.call、ES2015 展开运算符(spread operator)三种转换方案,并结合仓库内其他技巧(如 arguments 泄漏与 V8 优化杀手、Array.from 去重、展开运算符)做纵深对照,读完即可在真实项目中安全、正确地完成 NodeList 到数组的转换。
为什么需要把 NodeList 转换成数组
querySelectorAll方法返回的对象被称为 NodeList,它是一种"类数组"数据结构:可以用下标nodelist[0]访问元素,也有length属性,表面上看起来和数组几乎一样。但它并不继承Array.prototype,因此无法直接调用map、forEach、slice、filter等数组方法。
const nodelist = document.querySelectorAll('div'); // 下面的调用会报错:nodelist.forEach is not a function // nodelist.forEach(el => console.log(el));此时常见的做法是先把 NodeList 转换为一个真正的数组,再使用数组的完整方法集。原技巧(2016-01-08-converting-a-node-list-to-an-array.md)给出了三种方案:Array.apply(null, nodelist)、Array.prototype.slice.call(nodelist)以及 ES2015 的展开运算符[...nodelist]。下面逐一展开。
方案一:Array.apply(null, nodelist)
核心写法
const nodelist = document.querySelectorAll('div'); const nodelistToArray = Array.apply(null, nodelist); // 之后就可以放心使用数组方法了 nodelistToArray.forEach(...); nodelistToArray.map(...); nodelistToArray.slice(...); // 等等...原理拆解
Function.prototype.apply的第二个参数正是"类数组对象",它会被当作一组参数逐一传给被调用的函数。这里被调用的是Array构造函数,因此Array.apply(null, nodelist)等价于用 NodeList 中的每一个元素作为参数去调用Array,最终产出一个包含全部 DOM 元素的真实数组。
因为我们在本例中并不需要关心构造函数内部的this值,所以this位置传入null或0均可。这一点原文档也明确强调:MDN 规定apply接受类数组对象,恰好querySelectorAll返回的就是这类对象。
需要特别说明:apply接收类数组时,会使用其length属性来确定参数个数。多数浏览器对单次函数调用的参数数量存在上限(通常在数万量级),因此当页面中匹配的节点数量极大时,这种方法可能触及参数上限。对日常场景(几十到几千个节点)完全够用,但理解这个边界有助于在极端场景下选择合适的方案。
方案二:Array.prototype.slice.call(nodelist)
核心写法
const nodelist = document.querySelectorAll('div'); const nodelistToArray = Array.prototype.slice.call(nodelist); // 等价写法:Array.prototype.slice.apply(nodelist); // 之后就可以放心使用数组方法了 nodelistToArray.forEach(...); nodelistToArray.map(...); nodelistToArray.slice(...); // 等等...原理拆解
slice本身是数组的方法,其工作方式是"从this上按索引复制出一段新数组"。通过Function.prototype.call把 NodeList 临时"借"给slice作为this,slice就会按照 NodeList 的索引与length复制出一个真正的数组。由于slice内部并不真正修改this,只是读取索引与长度,因此对类数组对象同样适用。
这一招在 jstips 仓库中反复出现,是被广泛使用的通用模式:
- 在 第 31 期"避免修改或传递 arguments 导致优化失效" 中,函数内部的
arguments同样是类数组对象,文档给出的转换代码正是var args = Array.prototype.slice.call(arguments),并补充了更简短的写法var args = [].slice.call(arguments)(即从一个空数组字面量上调用slice); - 在 第 29 期"用记忆化加速递归函数" 中,也用
Array.prototype.slice.call(arguments)把参数快照成数组,再JSON.stringify作为缓存 key; - 在 第 42 期"防止 unapply 攻击" 中,同样依赖
Array.prototype.slice.call(arguments, 1)这类写法。
可见,掌握了slice.call,你不仅会转换 NodeList,还能顺手解决arguments等一切类数组对象的转换问题。
性能注意事项
结合仓库第 31 期的背景知识需要提醒:把函数内的arguments对象原样传给其他函数(即"泄漏 arguments")会导致 Chrome/Node 使用的 V8 引擎跳过对该函数的优化,带来明显的性能损耗。因此在普通函数内部将arguments转为数组时,第 31 期文档建议改用显式循环拷贝的方式:
var args = new Array(arguments.length); for (var i = 0; i < args.length; ++i) { args[i] = arguments[i]; }需要区分的是:本技巧讨论的querySelectorAll返回的 NodeList 并非函数内部的arguments,不存在该优化限制,直接用slice.call即可。只有当你在转换arguments时才需要考虑上述性能细节。
方案三:ES2015 展开运算符(推荐)
核心写法
const nodelist = [...document.querySelectorAll('div')]; // 返回一个真正的数组 // 之后就可以放心使用数组方法了 nodelist.forEach(...); nodelist.map(...); nodelist.slice(...); // 等等...原理与优势
展开运算符会把可迭代对象(iterable)的元素逐个展开放进一个新数组字面量中。NodeList 实现了可迭代协议(Symbol.iterator),因此[...nodelist]可以直接生成真实数组,这也是现代代码中最简洁、最直观的写法。
仓库中的 第 78 期"什么是展开运算符" 对它有系统介绍:展开运算符可以拼接数组([...arr1, ...arr2])、拷贝数组元素([...arr])、在函数调用中展开参数等。除了生成数组,它同样适用于其他可迭代对象(如Set、Map、字符串),一技多用。
对于运行环境支持 ES2015(现代浏览器与 Node 6+ 均支持)的项目,这是最推荐的选择。如果项目仍需要兼容老旧环境,则退回到方案二。
补充方案:Array.from
虽然原文档未提及,但作为同一主题的现代补充值得一并掌握。ES2015 还提供了Array.from,专门用于把类数组对象或可迭代对象转换成数组:
const nodelistToArray = Array.from(document.querySelectorAll('div'));仓库在 第 37 期"数组去重" 中使用了同款能力:var deduped = Array.from( new Set([1, 1, 'a', 'a']) ),而在 第 33 期"一行生成 0..N 区间" 中则用Array.from(new Array(N), (val, index) => index)构造数字区间。Array.from还支持第二个映射函数参数(类似map),例如在转换的同时过滤或改写元素。
// 转换并同时取出每个节点的文本内容 const texts = Array.from(document.querySelectorAll('p'), el => el.textContent);四种方案对比与选型建议
| 方案 | 写法 | 适用环境 | 特点 |
|---|---|---|---|
Array.apply(null, nodelist) | Array.apply(null, nodelist) | 所有环境 | 原文档首选,简单直接;极端大列表时受参数数量上限影响 |
slice.call | Array.prototype.slice.call(nodelist) | 所有环境 | 最经典的通用模式,同法可转换arguments等类数组 |
| 展开运算符 | [...nodelist] | ES2015+ | 最简洁直观,推荐现代项目使用 |
Array.from | Array.from(nodelist) | ES2015+ | 语义最明确,支持映射回调,一行完成转换 + 加工 |
选型建议:
- 现代项目:优先
[...nodelist]或Array.from(nodelist),代码可读性最佳; - 需要兼容旧浏览器或旧 Node:使用
Array.prototype.slice.call(nodelist); - 需要在转换的同时做映射加工:使用
Array.from(nodelist, mapper)。
总结
NodeList 的"类数组"特性是 DOM 编程中高频踩坑点。本文覆盖了 jstips 第 8 期给出的三种转换方法(Array.apply、slice.call、展开运算符),并补充了Array.from与现代写法对比。核心要点:
- 类数组对象拥有索引与
length,但不继承数组方法,必须先转换再使用map/forEach/slice等; Array.apply(null, nodelist)与Array.prototype.slice.call(nodelist)是环境兼容性最好的经典方案,后者还可复用于arguments;- ES2015 环境下
[...nodelist]与Array.from(nodelist)更简洁,后者还支持映射回调; - 转换函数内部
arguments时需注意 V8 优化杀手问题,建议参考 第 31 期 的显式循环拷贝方案。
在 jstips 仓库中,本技巧的英文原文位于 _posts/en/javascript/2016-01-08-converting-a-node-list-to-an-array.md,并同时提供 简体中文、西班牙语、繁体中文 等多语言版本,可对照阅读。
- 教程
【免费下载链接】jstips
This is about useful JS tips!
相关推荐
把 RK3568 的 DG-TN3568 接入 amlogic-s9xxx-armbian:一次完整适配实战
把 RK3568 的 DG TN3568 接入 amlogic s9xxx armbian:一次完整适配实战 amlogic s9xxx armbian 本是给
教程jstips 精讲(第 30 期):用 `!!` 操作符将 truthy/falsy 值转换为布尔值
jstips 精讲(第 30 期):用 !! 操作符将 truthy/falsy 值转换为布尔值 导读 在 JavaScript 开发中, if value 这
教程jstips 第 38 期:JavaScript 多维数组扁平化的五种实用方案
jstips 第 38 期:JavaScript 多维数组扁平化的五种实用方案 导读 本文基于 jstips 仓库第 38 号技巧( 西班牙语原文 https:
教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考