compareDocumentPosition

JavaScript

在判断一个 DOM 节点是否包含另一个节点的时候,常常用到 contains 这个 API。在实际的使用从过程中,也经常会遇到这样的情况,需要判断 A 是否包含 B,返回是 false,但经过排查,发现其实 A 和 B 就是同一个节点。这种情况下,光用 contains API 就有点不够用了。同时,也暴露了这个 API 本身能力的局限性。

在 DOM Level 3 的规范中,定义了一个新的 API,compareDocumentPosition。相比于 containscompareDocumentPosition 提供了更强大的判断结果。

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 表示两个节点不再同一个文档中,有几种可能的情况:

  1. A 和 B 中某一个存在于 iframe 中,因而两者不属于同一个文档(A.ownerDocument !== B.ownerDocument);
  2. A 和 B 中某一个元素被删除了(或没有插入到 DOM 中),导致两者不属于同一个文档(可以通过 A.parentElementB.parentElement 判断是否被删除,被删后就没有父元素了)

另外,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC 有两种情况:

  1. A 和 B 没有任何相同的 container,这种情况和 Node.DOCUMENT_POSITION_DISCONNECTED 是等价的。换句话说,当有 Node.DOCUMENT_POSITION_DISCONNECTED 的时候,一定同时有 Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
  2. 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 的情况。