你不知道的document对象(下):web标准是如何组装document对象的?

298 阅读8分钟

本文为HTML标准解读系列文章,其他文章详见这里

我们都知道document对象有很多的“乱七八糟”的属性方法,至于数量有多少呢?我们可以在一个空页面中运行以下代码大概计算一下:

for (let _ in document) count++
console.log(count) // 281

在我自己本地的chrome上测试,count最后的值为281。但因为我这里用的是for...in...,所以281还不包括不可枚举属性以及Symbol。

如此庞大的数目,会直接把想要完整了解document对象这些属性方法的人劝退。不过,我们并不是毫无办法,分类是人类处理复杂问题时候的有效手段。就好比我们会用“界门纲目科属种”给数以亿计的自然界生物进行归类,如果我们能够找到一种有意义的方法,对document的这些属性方法进行拆解,这会极大地降低我们的认知成本。

而本文,我将会以web标准对document接口拆分的方式作为分类的依据,力求给你呈现:

  • document对象的这些属性和方法都是从哪里来的?
  • web标准之间是如何进行协作的?

从原型链上进行分解

对于一个HTML文档来说,document对象的原型链是这样的:

[Object.prototype]
     ↑
[EventTarget.prototype]
     ↑
[Node.prototype]
     ↑
**[Document.prototype]** 
     ↑
HTMLDocument.prototype] ← [document的实例]

从原型链上我们可以看到:一个document对象,跟一个HTML元素一样,本质上都是一个Node节点,只不过节点类型不同。所以document对象同时也继承了Node接口上的所有属性方法。而Node接口上的属性方法,如document.nodeType,不属于本文的探讨范围,所以我们可以把它们先排除在外。

另一个方面,根据标准,HTMLDocument接口是一个历史遗留接口,本质上等同于Document接口。

基于以上,我们可以把注意力全部放在Document接口上,通过执行以下的代码,我们就可以看到真正属于Document接口自身的属性方法有哪些:

const documentInterfaceOwnProps = Object.getOwnPropertyNames(Object.getPrototypeOf(new Document))
console.log(documentInterfaceOwnProps.length)

Object.getOwnPropertyNames()方法会遍历不可枚举的属性,但不会遍历原型链上的属性。在不同的浏览器上,得到的数组会略微不同。在我本地的chrome上,我得到的数组最终长度而231,这231个属性方法就是我们今天要研究的内容。

web标准对接口的“组装”方式

web标准是使用web IDL来定义一个对象的属性和方法的,这些属性方法也统称为「成员」。(如果你不了解IDL片段又或者不知道怎么读,建议先阅读我的另一篇文章,我在那里对此作了深入的讲解。)

像document这样成员数量庞大的接口,它的定义不是在一个IDL片段里完成的,也不是在一个标准内完成的。而是先定义好一个原始的document接口,然后给这个接口预留一些扩展点,其他的标准根据需求在这些扩展点上面进行扩展。 所以document接口实际是被拆分成多个IDL片段,遍布在不同web标准的各个角落里。比如,以下是所有散落DOM标准和HTML标准中的「Document接口碎片」:

当你把所有web标准中的「Document接口碎片」集齐,就能召唤出一个完整的document对象。

HTML标准与DOM标准中的「Document接口碎片」

web IDL对一个接口进行扩展有两种方法:

  1. 对原始接口本身进行扩展。
  2. 对接口所组合的mixin接口进行扩展。

所有的Document接口碎片,都用通过这两种方式“组装”起来的。

对原始接口本身进行扩展

因为document对象本质上是一个node节点,所以最原始的IDL片段来自于DOM标准:

interface Document : Node {
	construtor()
	// ...
}

DOM标准中的原始Document接口定义了如下的成员:

document

通过使用web IDL中的partial接口,可以直接对原始接口进行扩展。格式如下:

partial interface Document {
	// ...
}

HTML标准中有两处使用了partial接口对Document接口进行扩展:

html_interface

对接口所组合的mixin接口进行扩展

mixin接口的作用增加描述接口的内聚性。

比如,DOM标准定义了一个DocumentOrShadowRoot mixin接口

interface mixin DocumentOrShadowRoot {
};
Document includes DocumentOrShadowRoot;
ShadowRoot includes DocumentOrShadowRoot;

你会发现,这个接口的成员是空的,这其实是DOM标准刻意预留的扩展点。未来只要其他的web标准在DocumentOrShadowRoot上面进行扩展(使用partial mixin),新的成员就会同时放进Document和ShadowRoot这两个接口当中。比如,HTML标准里面就对DocumentOrShadowRoot进行了扩展

partial interface mixin DocumentOrShadowRoot {
  readonly attribute Element? activeElement;
};

所以,Document接口和shadowRoot接口也会同时有activeElement的属性。

mixin_example1

在HTML标准和DOM标准中,Document接口组合的所有mixin接口如下图所示:

mixin_example2

遍布在其他web标准/草案中的「Document接口碎片」

如果你认为收集完HTML标准与DOM标准里的接口碎片就完事大吉了,那可太天真了。我把HTML标准和DOM标准定义的Document成员全部加起来,只有163个,离我们在上面算出来的231个还差很远。而剩下的成员,就真的是散落在各种各样的web标准/草案当中,有的你可能很熟悉,有的则是你从未听过的;有的已经成熟了,有的才刚刚起草。

比如,FullScreen API标准同时对「Document接口」和「DocumentOrShadowRoot mixin接口」进行扩展,增加了控制页面全屏的API:

partial interface Document {
  [LegacyLenientSetter] readonly attribute boolean fullscreenEnabled;
  [LegacyLenientSetter, Unscopable] readonly attribute boolean fullscreen; // historical
	// ...
};

partial interface mixin DocumentOrShadowRoot {
  [LegacyLenientSetter] readonly attribute Element? fullscreenElement;
};

又比如,为了应对近年来出现越来越多不同形式的输入设备(如鼠标、触摸屏、笔等等),Pointer event草案对于GlobalEventHandlers mixin接口进行了扩展,企图对不同的输入设备统一使用一种事件监听:

partial interface mixin GlobalEventHandlers {
    attribute EventHandler onpointerover;
    attribute EventHandler onpointerenter;
    attribute EventHandler onpointerdown;
    attribute EventHandler onpointermove;
		// ...
};

除此以外,还有很多很多来自其他标准/草案的成员,一图胜过千言万语,请看下图以及图后面为你准备的表格:

all_spec

上图我给你总结了所有document接口关联的标准/草案,下面这张表我贴上了这些标准/草案的内容以及链接:

名称内容
CSSOM草案定义CSS媒体查询、CSS选择器、CSS自身相关的API。
CSSOM View Module草案定义查看document视口的API,包括通过脚本获得元素布局的位置、获得视口的尺寸以及滚动一个元素。
Pointer Events草案为了应对近年来出现越来越多不同形式的输入设备(如鼠标、触摸屏、笔等等),该草案希望统一一种事件监听机制(Pointer event),而不是对不同的输入使用不同的事件监听机制(Touch event、Mouse Event)来处理。
Pointer Lock草案针对特定应用程序,尤其是3D应用所设计的API。可以把鼠标锁定在一个目标上,通过拖动鼠标转换视角。
Selection API 草案定义可以灵活选择document部分内容(用于复制、粘贴或编辑)的API。
Full Screen API 标准定义全屏相关的API。
Web Animations草案定义了一个页面动画呈现的模型,以及操作这个模型的相关API。使用这些API可以直接操作CSS过渡、CSS动画、SVG动画。
Picture-in-Picture草案定义视频窗口播放的API。
CSS Font Loading Modole草案定义动态加载文字资源的相关事件与接口。
CSS Transitions草案让CSS的值在一定时间内丝滑过渡。
Page LifeCycle草案定义管理页面生命周期的相关API。
CSS Animations草案使用keyframes来管理CSS动画。
Text Fragment草案增加URL片段(fragment)的能力,使得使用片段导航的时候,可以快速强调一些地方,获取用户的注意力。
The Storage Access API草案定义iframe请求访问宿主数据的API。

漏网之鱼

即便你收集完所有的接口碎片,你会发现还是有一些落单的属性方法。所有的这些属性方法,可以把他们按照以下方式进行归类:

  1. legacy属性:比如像xmlEncodingxmlStandaloneheightwidth这种属性,他们存在的意义只是为了向下兼容,各大标准早就把他们剔除掉了。
  2. 非标准属性:这种属性往作为一种试验性的功能,还没进入到各种规范中。如caretRangeFromPointonsearch等等。
  3. 浏览器差异属性: 由于浏览器内核的差异,一些属性在命名上也有区别。比如,在chrome和safari上,有一批以webkit开头的属性,如webkitFullscreenElement。同样的API在firefox上则是以moz开头,如mozFullScreenElement

总结

本文,我给你展示了Document对象是如何像搭乐高一样一步一步地被“组装”起来的。同时,这也是各种web标准之间的一种协作方式,在可预见的未来,Document对象上的属性方法必定会越来越多。

而另一个比document对象成员数量还要多的是window对象,有上千个属性方法。不过,他的「组装逻辑」跟document接口是一样的,如果你是一个很有耐心的人,你完成可以像我这样去收集这些属性方法的来源,你的收获一定会超过你的付出!