DOM是JavaScript中重要部分之一,在DOM中有一个动态集合。这个动态集合包含节点的集合(NodeList
)、元素属性的集合(NamedNodeMap
)和HTML元素的集合(HTMLCollection
)。这三个对象都是类数组(Array-like
),具有像数组一样的特性。更为重要的是,它们都是动态的,是有有生命有呼吸的对象,会实时更新查询DOM结构。今天我们学习的目标就是深究这三个动态集合之间的用法和联系以及区别。
类数组
文章开头就提到了,DOM中的动态集合都是一个对象,而且是一个类数组。那么什么是类数组呢?
对于类数组,简单的描述:
JavaScript中的对象看起来像却又不是数组的对象。
JavaScript的一个类数组对象有两个典型的特性:
- 具有:指向对象元素的数字索引下标以及
length
属性告诉我们对象的元素个数 - 不具有:不具有诸如
push()
、forEach()
以及indexOf()
等数组对象具有的方法
JavaScript中所说的这些类数组对象有一些,其中包括arguments
,arguments
是一个很特殊的变量,在所有的函数体内都可以访问到。比如:
let testFun = function () {
console.log(arguments)
console.log(arguments.length)
}
但如果我们在控制器中输入arguments.shift()
是将会报错:
Uncaught TypeError: arguments.shift is not a function
但shift()
是数组的一个函数。我们在尝试一下,在函数体内打印arguments.constructor
和[].constructor
,分别会打印出Object()
和Array[]
:
let testFun = function () {
console.log(arguments)
console.log(arguments.length)
console.log(arguments.constructor)
console.log([].constructor)
}
从结果上看,是不是觉得很奇怪?
这不仅局限于argumetns
,在DOM中的很多集合都会返回这种对象(类数组对象),比如document.getElementsByTagName()
、document.images
和document.childNodes
等。
这里也提到了,假如我们在操作DOM的时候,使用了document.getElementsByTagName()
得到的是一个类数组对象,要操作DOM,又避免不了对这个类数组对象进行操作。那么问题又来了,类数组对象是不具备数组中的方法。这样一来,就需要让类数组对象转换为一个数组。
将类数组对象转换为数组最经典的一个方法就是使用Array
的slice()
方法:
var arr = Array.prototype.slice.call(arguments);
// 等同于
var arr = [].slice.call(arguments)
另外在ES6中,可以使用Array.from()
方法:
var arr = Array.from(arguments);
只要有length
属性的对象,都可以应用这个方法转换成为数组。除此之外,还可以使用ES6中的扩展运算符...
将某些数据结构转换成数组,这种数据结构必须有遍历器接口。
var args = [...arguments];
DOM中的动态集合
为了更好的阐述后面的内容,我们之后的示例,都会用到下面这个HTML结构:
<!DOCTYPE html>
<html>
<head>
<title>DOM Tree Sample Document</title>
</head>
<body>
<div id="box">
<!-- 标题 -->
<div>Title</div>
<div class="item">Item1</div>
<div class="item">Item2</div>
<div class="item">Item3</div>
<div class="item">Item4</div>
<div class="item">Item5</div>
</div>
</body>
</html>
NodeList集合
在《初识JavaScript的DOM》一节中,我们知道了DOM将HTML页面解析成一个由多层次节点构成的结构。节点是页面结构的基础,而所有节点继承自NOde
类型,因此所有节点共享着基本的属性和方法。
其中NodeList
是node
节点的集合,用于保存一组有序的节点,可以通过节点的位置访问这些节点。而且NodeList
是一种类数组对象。Node
类型有一个childNodes
属性,通过这个属性可以得到一个保存着本节点的子点节点组成的NodeList
对象。除此之外,还可以使用querySelectorAll()
方法返回值中保存着NodeList
对象。
比如上面的示例代码,先看childNodes
属性中的NodeList
对象:
let box = document.getElementById('box')
let children = box.childNodes;
console.log(children, children.length)
console.log(children instanceof NodeList)
输出的结果如下:
再来看document.querySelectAll()
方法返回值中的NodeList
对象:
let divs = document.querySelectorAll('div')
console.log(divs, divs.length)
console.log(divs instanceof NodeList)
Node
的childNodes
和document.querySelectAll()
对应的NodeList
是有所不同的,前者是动态的,后者是静态的。比如:
甚至什么是动态NodeList
,什么是静态NodeList
?后续会阐述。这里暂时不深究。
NodeList
可以通过[]
表达式来访问,也可以通过item()
方法来访问。而且它也有length
属性,可以访问元素个数。虽然JavaScript中的数组可以修改length
属性,但NodeList
是一个类数组,而且它是页面一片区域的DOM结构映射。所以不要修改NodeList
对象的length
值。
console.log('First Child:', children[0])
console.log('Second Child:', children.item(1))
console.log('Last Child:', children[children.length - 1])
HTMLCollection集合
HTMLCollection
对象与NodeList
对象类似,都是节点的集合,返回的都是类数组对象。但也有其不同之处,其中NodeList
集合包含着node
节点中12
种节点,而HTMLCollection
仅包含elements
元素节点的集合。
HTMLCollection
的集合可以通过getElementsByTagName()
、getElementsByName()
、document.anchors
、document.forms
、document.images
和documnet.links
等方式来获取。比如:
// 获取NodeList
let nodeList = document.getElementById('box').childNodes
console.log(nodeList, nodeList.length)
// 获取HTMLCollection
let htmlCollectionList = document.getElementsByTagName('div')
console.log(htmlCollectionList, htmlCollectionList.length)
HTMLCollection
和NodeList
类似,都是类数组,同样可以使用[]
或者item()
来访问。
console.log('First Element:', htmlCollectionList[0])
console.log('Last Element:', htmlCollectionList.item(htmlCollectionList.length - 1))
HTMLCollection
和NodeList
都是DOM的节点集合;但是它们两个能够包含的元素是不太一样的,HTMLCollection
只可以包含HTML元素(Element
)集合,NodeList
可以包含任意的节点类型,就是说NodeList
不仅可以包含HTML元素集合,也可以包含像文字节点,注释节点等类型的节点集合。
从上图可以看到,就上例而言,nodeList
是一个NodeList
集合,它包含了8
个text
节点(TEXT_NODE = 3
),一个comment
节点(COMMENT_NODE = 8
)和6
个div
元素节点(ELEMENT_NODE = 1
);htmlCollectionList
是一个HTMLCollection
集合,它只包含了7
个div
元素(ELEMENT_NODE = 1
)。
HTMLCollection
和NodeList
还有一个不同之处就是多一个namedItem
方法,其它的方法它们两个都相同的。有关于这两者更深入的介绍,可以查阅下面的资料:
NameNodeMap集合
DOM中的Element
节点是唯一拥有attributes
属性的一种节点类型。而attributes
属性中就包含NamedNodeMap
集合。NamedNodeMap
集合的元素拥有nodeName
和nodeValue
属性,分别表示元素节点名称和值。
三者的异同
虽然NodeList
、HTMLCollection
和NamedNodeMap
都是DOM的动态集合,但三者之间也有差异。先来看三者相同之处:
- 三者都具有
length
属性 - 三者都有
item()
方法 - 三者都是动态的,如果对
NodeList
和HTMLCollection
中的元素进行操作都会直接反映到DOM中,因此如果一次性直接在集合中进行DOM操作,开销非常大
另外三者也有不同之处:
NodeList
里面包含了所有的节点类型HTMLCollection
里面只包含元素节点NamedNodeMap
里面包含了Attribute
的集合,例如id
、title
、class
等,集合中的每一个元素都是attr
类型- 三者所提供的方法也有不同,例如
HTMLCollection
中提供了namedItem()
,而NodeList
和NamedNodeMap
两个集合中没有namedItem()
方法
将动态集合类数组转换为数组
文章开头了解arguments
对象时都知道它是一个类数组对象,有数组的表达式,但没有数组方法。而DOM的三个动态集合HTMLCollection
、NodeList
和NodeNameMap
与arguments
对象一样,也是类数组。因此必须将类数组转换为DOM元素的数组。拿NodeList
为例:
const nodeList = document.querySelectorAll('div');
const nodeListToArray = Array.apply(null, nodeList);
//之后 ..
nodeListToArray.forEach(...);
nodeListToArray.map(...);
nodeListToArray.slice(...);
apply
方法可以在指定this
时以数组形式向方法传递参数。MDN规定apply
可以接受类数组对象,恰巧就是querySelectorAll
方法所返回的内容。如果我们不需要指定方法内this
的值时传null
或0
即可。返回的结果即包含所有数组方法的DOM元素数组。
另外你可以使用Array.prototype.slice
结合Function.prototype.call
或Function.prototype.apply
, 将类数组对象当做this
传入:
const nodeList = document.querySelectorAll('div');
const nodeListToArray = Array.prototype.slice.call(nodeList);
// 等价于
// const nodeListToArray = Array.prototype.slice.apply(nodeList);
//之后 ..
nodeListToArray.forEach(...);
nodeListToArray.map(...);
nodeListToArray.slice(...);
如果你正在用ES6你可以使用展开运算符 ...
:
// 返回一个真正的数组
const nodeList = [...document.querySelectorAll('div')];
//之后 ..
nodeList.forEach(...);
nodeList.map(...);
nodeList.slice(...);
为了方便操作或者之后更易复用,可以写一个转换函数convertToArray()
:
function convertToArray(nodes) {
var array = null
try {
array = nodes.prototype.slice.call(nodes, 0)
} catch {
array = new Array()
for (let i = 0, len = nodes.length; i < len; i++) {
array.push(nodes[i])
}
}
}
动态NodeList和静态NodeList
前面提到过,getElementsByTagName()
方法返回一个动态(live)的NodeList
,而querySelectorAll()
返回的是一个静态(static)的NodeList
。那么什么是动态的NodeList
,什么又是静态的NodeList
,他们有何区别呢?接下来,花点时间了解一下。
动态NodeList
动态的NodeList
是DOM中的一个大坑。NodeList
对象以及HTMLCollection
对象是一种特殊类型的对象。DOM3规范对HTMLCollection
对象的描述如下:
DOM中的
NodeList
和NamedNodeMap
对象是动态的;也就是说,对底层文档结构的修改会动态地反映到相关的集合NodeList
和NamedNodeMap
中。例如,如果先获取了某个元素(Element
)的子元素的动态集合NodeList
对象,然后又在其他地方顺序添加更多子元素到这个DOM父元素中(可以说添加、修改、删除子元素等操作),这些更改将自动反射到NodeList
,不需要手动进行其他调用。同样地,对DOM树上某个Node
节点的修改,也会实时影响引用了该节点的NodeList
和NamedNodeMap
对象。
上面的大概意思就是说,DOM中的NodeList
是一种特殊的对象,它是实时更新的,就是你对这个NodeList
中的任何一个元素进行的一些操作,都会实时的更新到这个NodeList
对象上面。比如下面这个例子:
let box = document.getElementById('box')
let liveNodeList = document.getElementsByTagName('div')
console.log(liveNodeList, liveNodeList.length)
let newEle = document.createElement('div')
newEle.textContent = '新创建的div元素'
box.appendChild(newEle)
console.log(liveNodeList, liveNodeList.length)
上图已经很允分的说明了liveNodeList
是一个动态的NodeList
或者说HTMLCollection
。第一次打印出liveNodeList
的时候,它的length
值为7
,也就是说,这个时候这个集合里面有七个元素;但经过后面的操作,添加了一个新的div
元素,这个操作会实时的反映到这个对象身上。然后就会出现了上面的那种情况。
上面示例中getElementsByTagName()
方法返回对应在标签名的元素的一个动态集合,只要document
发生了变化,就会自动更新对应的元素。那么一不小心就会进入一个死循环。比如:
var liveNodeList = document.getElementsByTagName('div')
var i = 0
while(i < liveNodeList.length) {
document.getElementById('box').appendChild(document.createElement('div'))
i++
}
死循环的原因是每次循环都会重新计算 liveNodeList.length
。 每次迭代都会添加一个新的 <div>
, 所以每次 i++
,对应的 liveNodeList.length
也在增加, 所以 i
永远比liveNodeList.length
小, 循环终止条件也就不会触发(例外的情况是DOM中没有div
,不进入循环)。
你可能会觉得这种动态集合是个坏主意, 但通过动态集合可以保证某些使用非常普遍的对象在各种情况下都是同一个, 如 document.images
, document.forms
, 以及其他类似的 pre-DOM集合。
静态NodeList
前面提到过querySelectorAll()
方法将会返回一个静态的NodeList
。
W3C规范是这样描述静态NodeList
的:
querySelectorAll()
方法返回的NodeList
对象必须是静态的,而不能是动态的。后续对底层document
的更改不能影响到返回的这个NodeList
对象。这意味着返回的对象将包含在创建列表那一刻匹配的所有元素节点。
上面的大概意思就是说,通过使用querySelectorAll()
方法返回的NodeList
集合必须是静态的,就是一旦获取到这个结果;那么这个结果不会因为后面再对这个集合中元素进行的操作而进行改变。我们可以改变一下上面的例子:
let box = document.getElementById('box')
let liveNodeList = document.querySelectorAll('div')
console.log(liveNodeList, liveNodeList.length)
let newEle = document.createElement('div')
newEle.textContent = '新创建的div元素'
box.appendChild(newEle)
console.log(liveNodeList, liveNodeList.length)
liveNodeList = document.querySelectorAll('div')
console.log(liveNodeList, liveNodeList.length)
上面这张图片展示的结果跟我们的预期是一样的,也就是说,静态的NodeList
集合,一旦获取到结果,就不会再次因为这个集合中的元素发生变化而发生改变。
所以即便是让 querySelectorAll()
和 getElementsByTagName()
具有相同的参数和行为, 他们也是有很大的不同点。 在前一种情况下, 返回的 NodeList
就是方法被调用时刻的文档状态的快照, 而后者总是会随时根据document
的状态而更新。 下面的代码就不会是死循环:
var liveNodeList = document.querySelectorAll("div"),
i=0;
while(i < liveNodeList.length){
let newEle = document.createElement('div')
newEle.textContent = 'new ele' + i
document.getElementById('box').appendChild(newEle)
i++;
}
在这种情况下没有死循环, liveNodeList.length
的值永远不会改变, 所以循环实际上就是将 <div>
元素的数量增加一倍, 然后就退出循环。
为什么动态NodeList比静态NodeList更快
动态 NodeList
对象在浏览器中可以更快地被创建并返回,因为他们不需要预先获取所有的信息, 而静态 NodeList
从一开始就需要取得并封装所有相关数据. 再三强调要彻底了解这一点, WebKit 的源码中对每种 NodeList
类型都有一个单独的源文件: DynamicNodeList.cpp和 StaticNodeList.cpp。两种对象类型的创建方式是完全不同的。
DynamicNodeList
对象通过在cache
缓存中 注册它的存在并创建。 从本质上讲, 创建一个新的 DynamicNodeList
是非常轻量级的, 因为不需要做任何前期工作。 每次访问 DynamicNodeList
时, 必须查询 document
的变化, length
属性 以及 item()
方法证明了这一点(使用中括号的方式访问也是一样的)。
相比之下, StaticNodeList
对象实例由另一个文件创建,然后循环填充所有的数据 。 在 document
中执行静态查询的前期成本上比起 DynamicNodeList
要显著提高很多倍。
如果真正的查看WebKit的源码,你会发现他为 querySelectorAll()
明确地 创建一个返回对象 ,在其中又使用一个循环来获取每一个结果,并创建最终返回的一个 NodeList
。
可以这样来理解:
因为通过getElementsByTagName()
获取到的NodeList
是一个实时的集合,这种动态的集合,是不需要在一开始的时候就获取到所有的信息的;然而通过querySelectorAll()
方法获取到的的NodeList
集合是一个静态的集合,这个集合相当于一个快照,就是在这个方法运行的那个时间,它所要获取的集合元素的一个快照,所以这个集合要保存大量的信息,速度自然会慢下来。
也就是说,
使用
getElementsByTagName()
方法我们得到的结果就像是一个对象的索引,而通过querySelectorAll()
方法我们得到的是一个对象的克隆;所以当这个对象数据量非常大的时候,显然克隆这个对象所需要花费的时间是很长的。
在以后需要用到获取元素集合的方法的时候,我们就要根据不同的场景来选择使用不同的方法了。如果你不需要一个快照,那就选择使用getElementsByTagName()
方法,如果你需要一个快照来进行复杂的CSS查询,或者复杂的DOM操作的话,那就选择使用querySelectorAll()
方法。
这也就是为什么说getElementsByTagName()
在所有浏览器上都比auerySelectorAll()
要快好多倍。
总结
DOM中有三个动态集合,它们分别是NodeList
、HTMLCollection
和NamedNodeMap
,而这三个集合都是类数组对象。具有数组的表现方式,但没有不具备数组的方法。在实际使用时,需要将类数组转换为数组。更为重要的是,它们都是动态的,是有有生命有呼吸的对象,会实时更新查询DOM结构。除此之外,动态集合将会有动态NodeList
和静态NodeList
之分,并且动态NodeList
要比静态NodeList
要快。其根本原因在于两者对象不同。这也是为什么说getElementsByTagName()
速度比 querySelectorAll()
快的根本原因所在。
如需转载,烦请注明出处:https://www.w3cplus.com/javascript/dom-dynamic-collection.html