在判断一个 DOM 节点是否包含另一个节点的时候,常常用到 contains
这个 API。在实际的使用从过程中,也经常会遇到这样的情况,需要判断 A 是否包含 B,返回是 false
,但经过排查,发现其实 A 和 B 就是同一个节点。这种情况下,光用 contains
API 就有点不够用了。同时,也暴露了这个 API 本身能力的局限性。
在 DOM Level 3 的规范中,定义了一个新的 API,compareDocumentPosition
。相比于 contains
,compareDocumentPosition
提供了更强大的判断结果。
compareDocumentPosition
这个 API 比较后会返回一个数字,通过二进制位的比较,可以用于判断两个节点之间的关系。假设调用的函数为 A.compareDocumentPosition(B)
,那么返回值具体支持的类型如下:
常量名 | 值 | 含义 |
---|---|---|
Node.DOCUMENT_POSITION_DISCONNECTED |
1 | 不在一个文档中 |
Node.DOCUMENT_POSITION_PRECEDING |
2 | B 在 A 之前 |
Node.DOCUMENT_POSITION_FOLLOWING |
4 | B 在 A 之后 |
Node.DOCUMENT_POSITION_CONTAINS |
8 | B 包含 A |
Node.DOCUMENT_POSITION_CONTAINED_BY |
16 | A 包含 B |
Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC |
32 | A 和 B 的位置关系取决于具体的实现方式(不由规范确定) |
这里之所以使用二进制位表示位置关系,一个很重要的原因就是:API 有可能会一次性返回多个结果。举个例子,假设 A.contains(B)
返回 true
。那么,在调用 A.compareDocumentPosition(B)
的时候,返回值是 20
,也就是 Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY
的结果。换句话说,B 元素在文档中的位置在 A 的后面,同时 B 也是 A 的一个子元素。
这里,Node.DOCUMENT_POSITION_DISCONNECTED
表示两个节点不再同一个文档中,有几种可能的情况:
- A 和 B 中某一个存在于 iframe 中,因而两者不属于同一个文档(
A.ownerDocument !== B.ownerDocument
); - A 和 B 中某一个元素被删除了(或没有插入到 DOM 中),导致两者不属于同一个文档(可以通过
A.parentElement
和B.parentElement
判断是否被删除,被删后就没有父元素了)
另外,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
有两种情况:
- A 和 B 没有任何相同的 container,这种情况和
Node.DOCUMENT_POSITION_DISCONNECTED
是等价的。换句话说,当有Node.DOCUMENT_POSITION_DISCONNECTED
的时候,一定同时有Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
; - A 和 B 是同一个元素的两个属性值,这种情况下,谁先谁后是由具体实现决定的。比如,
Element.attributes
返回一个NamedNodeMap
。根据规范 的定义,NamedNodeMap
不维护一个具体的顺序,但同时提供使用 index 访问的 API。也就是说,Element.attributes
中的任意两个字段,是没有定义上的先后之分的(虽然可能通过不同的下标获取到)。具体来说:
// div = <div id="id" class="class></div>
const attributes = div.attributes;
const result = attributes[0].compareDocumentPosition(attributes[1]);
// result = 36
console.log(result);
这里,compareDocumentPosition
返回的结果是 36
,即 Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_PRECEDING
。因此,在实际使用 API 的时候,有必要检查是否有 Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
这一位,如果有的话,其他的结果都可以忽略不计了。
另外,如果 A 和 B 是同一个元素,那么返回的结果将是 0
,因为 A 和 B 的关系不属于上面列出的任何一种情况。同时,也不难发现,只有当 A 和 B 是同一个元素的时候,才会出现返回值是 0 的情况。