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