“理解NodeList和HTMLCollection,是从整体上透彻理解DOM的关键所在。“
——《JavaScript高级程序设计》
解决上面的问题之后,我们有必要进一步深入一下。NodeList和HTMLCollection这两个类,在方法上的差异是毫无疑问的。按照Stack Overflow上的说法:
Both interfaces are collections of DOM nodes. They differ in the methods they provide and in the type of nodes they can contain. While a
NodeList
can contain any node type, anHTMLCollection
is supposed to only contain Element nodes.An
HTMLCollection
provides the same methods as aNodeList
and additionally a method callednamedItem
.Collections are always used when access has to be provided to multiple nodes, e.g. most selector methods (such as
getElementsByTagName
) return multiple nodes or getting a reference to all children (element.childNodes
).
大概是说,HTMLCollection比NodeList多了一个namedItem
方法;NodeList可以包含任何结点,而HTMLCollection只包含元素结点。
这些在前文已经提过了,而且有了TypeScript的类型,这些都是很显然的。说起来,这个item方法和方括号语法是等价的,倒有点像运算符重载……或者说,Python里的魔术方法?
事实上,NodeList还额外拥有一个forEach
方法,可以用来遍历。
如何获得这两个集合?方法很多,就不一一列举了。主要来说,有getElementsByClassName
、getElementsByName
、getElementsByTagName
、children
、childNodes
,等等。最神秘的是他们的返回值大多不相同,所以用的时候一定要慎重。至于为什么会这样……大概还是历史遗留问题吧。还好我没有生在那个各大浏览器混战的时代。
此外,还有一个很特殊的地方。可能你之前已经注意到了,这两个集合是动态的,会随着DOM的改变而改变。但是有一个方法例外:querySelectorAll
,这个方法返回的集合是静态的。但是,为什么呢?TypeScript也许可以给我们一个答案:
interface ParentNode {
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K)
: HTMLElementTagNameMap[K] | null;
querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K)
: NodeListOf<HTMLElementTagNameMap[K]>;
// ...
}
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"body": HTMLBodyElement;
"br": HTMLBRElement;
"button": HTMLButtonElement;
"div": HTMLDivElement;
"h1": HTMLHeadingElement;
"hr": HTMLHRElement;
"html": HTMLHtmlElement;
"img": HTMLImageElement;
"input": HTMLInputElement;
"li": HTMLLIElement;
"p": HTMLParagraphElement;
"span": HTMLSpanElement;
"strong": HTMLElement;
"style": HTMLStyleElement;
"table": HTMLTableElement;
"ul": HTMLUListElement;
// ...
}
因为同类和重载的方法比较多,就不一一列举了,只找其中一个来看;原理反正是一样的(笑)。HTMLElementTagNameMap
这个类其实就是一个标签到具体元素的映射。从方法中可以看出,querySelector
方法的返回值是HTMLElementTagNameMap[K]
,也就是对应元素的引用,所以是动态的;而querySelectorAll
返回的是NodeListOf<HTMLElementTagNameMap[K]>
,经过了一次包装(有了一个包装对象!),保存的是当时状态的快照,所以是静态的。
因为这两者之前存在差异,而且动态改变这个特性大多数时候都很烦人,因为我们往往需要的是一个静态的副本(或者叫快照snapshot吧),所以我们需要一个解决措施。可能大家都知道,我们可以将这个集合转换成数组:
Array.prototype.slice.call(collection);
但是这个方法并不很优雅。感觉上好像有一个简单一点的写法:
[].slice.call(collection);
比刚才那个好一点。事实上,到了ES6,我们还可以这么写:
Array.from(collection);
这个已经很好了。但是还有更tricky的:
[...collection]
利用解构运算符来实现优雅的转换集合,可以说是很有意思了。
参考资料
目录
从TypeScript视角看HTML DOM(二):Node与Element
从TypeScript视角看HTML DOM(四):Event Flow