DOM是JavaScript中重要部分之一,在DOM中有一个动态集合。这个动态集合包含节点的集合(NodeList)、元素属性的集合(NamedNodeMap)和HTML元素的集合(HTMLCollection)。这三个对象都是类数组(Array-like),具有像数组一样的特性。更为重要的是,它们都是动态的,是有有生命有呼吸的对象,会实时更新查询DOM结构。今天我们学习的目标就是深究这三个动态集合之间的用法和联系以及区别。
类数组
文章开头就提到了,DOM中的动态集合都是一个对象,而且是一个类数组。那么什么是类数组呢?
对于类数组,简单的描述:
JavaScript中的对象看起来像却又不是数组的对象。
JavaScript的一个类数组对象有两个典型的特性:
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结构:
<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对象类似,都是节点的集合js集合,返回的都是类数组对象。但也有其不同之处,其中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方法,其它的方法它们两个都相同的。有关于这两者更深入的介绍js集合,可以查阅下面的资料:
NameNodeMap集合
DOM中的Element节点是唯一拥有attributes属性的一种节点类型。而attributes属性中就包含NamedNodeMap集合。NamedNodeMap集合的元素拥有nodeName和nodeValue属性,分别表示元素节点名称和值。
三者的异同
虽然NodeList、HTMLCollection和NamedNodeMap都是DOM的动态集合,但三者之间也有差异。先来看三者相同之处:
另外三者也有不同之处:
将动态集合类数组转换为数组
文章开头了解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 = 0while(i < liveNodeList.length) { document.getElementById('box').appendChild(document.createElement('div')) i++ }
死循环的原因是每次循环都会重新计算liveNodeList.length。 每次迭代都会添加一个新的
, 所以每次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的值永远不会改变, 所以循环实际上就是将
元素的数量增加一倍, 然后就退出循环。
为什么动态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()快的根本原因所在。
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688