本文为HTML标准解读系列文章,其他文章详见这里。
在HTML标准的2.6小节,我们第一次遇到了IDL片段,他定义了HTMLALLCollection
的接口:
[Exposed=Window, LegacyUnenumerableNamedProperties]
interface HTMLAllCollection {
readonly attribute unsigned long length;
getter Element (unsigned long index);
getter (HTMLCollection or Element)? namedItem(DOMString name);
(HTMLCollection or Element)? item(optional DOMString nameOrIndex);
// Note: HTMLAllCollection objects have a custom [[Call]] internal method and an [[IsHTMLDDA]] internal slot.
};
接下来,像这样的IDL片段贯穿了整个标准,或长或短,或简单或复杂。于是,弄懂web IDL就变成了一个必须要做的事情了:
-
不仅仅是HTML标准,DOM标准、ECMAScript标准也是使用web IDL来定义接口的。如果你想读懂任何这些标准,就绕不开web IDL。
-
理解web IDL可以让你以更专业、更高效的方式了解一个标准定义的对象,而不是使用MDN这种二手资料。
-
理解web IDL有助于深刻理解接口之间的继承关系,增加知识碎片的连接,搭建健壮的知识网络。比如,当你在看一个HTMLCollection接口的时候,你会发现至少有这些方法/属性返回值是使用了这个接口:
// 元素搜索方法 document.getElementsByTagName() document.getElementsByTagNameNS() document.getElementsByClassName() // 获取一类元素 document.images document.embeds document.plugins document.links document.forms document.scripts document.applets document.anchors // 特定元素上的属性 map.areas table.tBodies table.rows tbody.rows tr.cells select.selectedOptions datalist.options fieldset.elements // node的属性 node.children
然后,你还可以看到有以下这些接口继承了HTMLCollection:
HTMLFormControlsCollection HTMLOptionsCollection
再进一步延伸,你还可以继续查看哪些对象和API使用了这些接口。于是,就是这样,原本看是毫无关联的知识便建立了正确且有意义的连接。
本文,我将会基于web IDL标准、并以HTML、DOM标准里面的几个IDL片段为例子,来为你提供理解web IDL的基本框架。我的目标是读者读完本文,能够明白web IDL大致怎么一回事,并有底气读懂所有的IDL片段。
为什么要有web IDL?
web IDL(Interface description language),是一门描述接口的语言。
在不同的场景下,接口有不同的含义。在硬件层面,我们有硬件接口,如USB接口。在人机交互方面,我们有User Interface(UI);在客户端和服务端之间,我们还有前后端接口;
但是web IDL所指的接口,是面向对象编程语言里,语言层面上的「接口」。以一个TypeScript的代码片段为例子:
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
// 报错:Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
pong() {
console.log("pong!");
}
}
这个片段定义了一个Pingable
的接口。这个接口规定了:所有实现(implements)这个接口的类,都必须有一个ping
方法,所以Sonar
可以正常被编译,而Ball
会报错。
这个Pingable
,就是面向对象编程语言中语言层面的「接口」。这么做的好处是:Sonar
类的使用者不需要知道ping
具体是怎么实现的,只需要知道他有一个ping
方法就可以了。当ping
方法的实现逻辑进行了变动,比如更换成process.stdout.write('ping!')
的时候,Sonar
类的使用者不需要修改代码来适应新的改动,从而降低了耦合性。
这是一种叫「基于接口而非实现编程」的编程风格:将接口和实现分离,封装不稳定的实现,暴露稳定的接口。 一些编程语言如Java,天生支持接口类,而像JavaScript这样的动态语言只能通过TypeScript或鸭子类型间接做到这一点。
同时,这也是每个web标准在做的事情:他们定义了各种各样ECMAScript对象接口、DOM元素接口,不同的浏览器是如何实现这些接口的,网页开发者不需要关心,开发者只需要基于接口的定义去进行编码即可。
又为了让这些接口的实现不与特定的语言绑定,于是就有了web IDL。web IDL定义了一套描述接口的语言规则,基于这套规则,你可以使用不同的编程语言来实现同样的接口。 于是,一方面你能看到浏览器是使用C++写的,另一方面,你又能看到像JSDOM这样使用nodeJS来实现DOM和HTML的项目。
web IDL语法概览
以下我将从四个我认为web IDL里最基本、最重要、出现频率最高的方面来展开,这四个部分也构成了整个web IDL的基本框架。它们分别是:
- 接口的成员(members)
- 接口的继承
- Extended Attribute 扩展属性
- Mixin接口和Partial接口
从属性、方法到所有成员
我们以开篇提到的HTMLCollection接口作为第一个例子:
interface HTMLCollection {
readonly attribute unsigned long length;
getter Element? item(unsigned long index);
getter Element? namedItem(DOMString name);
};
如果把这个IDL片段翻译成“人话”,大致是这样的:
这是一个名为
HTMLCollection
的接口。这个接口有以下成员:
一个只读属性:类型为
unsigned long
, 名为length
;一个名为
item
的方法,接受一个类型为unsigned long
、名为index
的参数,返回值的类型可能是Element
或者Null
;一个名为
nameItem
的方法,接受一个类型为DOMString
、名为name
的参数,返回值的类型可能是Element
或者Null
。
getter
关键词表示item
和nameItem
是特殊的方法,可以以属性的方式进行访问。所以collection.item(index)
与collection[index]
是等价的;collection.namedItem(name)
与collection[name]
是等价的。
你可以通过在本地测试来加深理解。比如document.images
的值是一个实现了HTMLCollection
接口的对象,于是,你可以通过Object.getPrototypeOf(document.images)
看到该接口声明的所有属性和方法。
一般来说,标准中不会只给你抛出一个IDL片段就完事了。如有必要,他会在片段下面解释每个属性或者方法的意义、调用的算法步骤等等。
在web IDL中,除了注释以外,所有被大括号{}
扩住的语句都被称之为成员(members) 。上面的IDL片段有两种类型的成员,length
属性的成员类型是regular attribute/常规属性
,而name
和nameItem
方法的成员类型是special operation/特殊操作
。web IDL定义了11种成员,我在文末为你总结了一张表格,列出了每一种成员的功能概括、格式、实际应用的例子,让你可以快速掌握所有的成员类型。
接口的继承
在web IDL中,如果一个接口继承另一个接口,会使用冒号:
表示,比如HTMLFormControlsCollection接口继承了HTMLCollection:
interface HTMLFormControlsCollection : HTMLCollection {
// inherits length and item()
getter (RadioNodeList or Element)? namedItem(DOMString name); // shadows inherited namedItem()
};
forms.elements
会返回一个实现了这个接口的对象,你可以在谷歌首页执行document.forms[0].elements
看到这一点。
接口的继承关系会在原型链上得到反映。HTMLFormControlsCollection实例的原型链是这样的:
[Object.prototype: Object的原型]
↑
[HTMLCollection.prototype: HTMLCollection的接口原型对象]
↑
[HTMLFormControlsCollection.prototype: HTMLFormControlsCollection的接口原型对象]
↑
[HTMLFormControlsCollection的实例]
基于接口的继承关系,你甚至可以拉出一条完整的HTML接口继承图谱。只不过看起来会很复杂。比如就有人用d3画了一张以EventTarget接口为起点的继承关系图 。
Extended Attribute 扩展属性
上面我为了讲解方便,刻意省略IDL片段中的一些内容,完整的HTMLCollection
的接口应该是这样的:
[Exposed=Window, LegacyUnenumerableNamedProperties]
interface HTMLCollection {
readonly attribute unsigned long length;
getter Element? item(unsigned long index);
getter Element? namedItem(DOMString name);
};
用[]
括起来的部分称为扩展属性 ,是web IDL中的一种标记方式,表示这个接口的具有一些特殊行为。
比如,HTMLCollection接口有两个扩展属性,一个是[Exposed=Window]
,另一个是[LegacyUnenumerableNamedProperties]
:
[Exposed=Window]
表示HTMLCollection接口的实例只能在主线程中使用,不能在worker中使用。如果一个接口的实例既能在worker中使用,也能在主线程中使用,那么需要用[Exposed=(Window,Worker)]
表示。[LegacyUnenumerableNamedProperties]
:在web IDL中,像item
这样可以通过index
属性来访问的getter方法称为index properties
;像nameItem
这样可以通过name
属性访问的getter方法称之为name properties
;[LegacyUnenumerableNamedProperties]
则表明这个接口中的name properties
是不可枚举的,所以使用Object.getOwnPropertyDescriptor
查看nameItem
对应的集合时,enumerable
的值是false
。
另一个例子,我在讲结构化克隆的时候提到过,标准使用[Serializable]
扩展属性标记一个可被序列化的接口,用[Transferable]
扩展属性来标记一个可转移对象。
当你在阅读IDL片段的时候,你会遇到大量的扩展属性。幸运的是,大部分扩展属性都是重复的,并且标准都会给你贴上对应解释的链接,所以我们只要沿着链接去理解,想要弄懂它的意义并不难。
mixin接口与partial接口
上面讲的3个方面,都是web IDL用来描述接口的某种特性的。而接下来讲的mixin接口和partial接口,纯粹是IDL为了提升描述接口时的简洁性所设计的一种辅助功能。
比如,HTMLBodyElement
接口元素使用了mixin:
[Exposed=Window]
interface HTMLBodyElement : HTMLElement {
[HTMLConstructor] constructor();
// also has obsolete members
};
HTMLBodyElement includes WindowEventHandlers;
这里的HTMLBodyElement includes WindowEventHandlers;
表示HTMLBodyElement
接口包含了WindowEventHandlers
mixin接口里所有的成员。web IDL使用interface mixin
来声明一个mixin接口,WindowEventHandlers
mixin接口如下:
interface mixin WindowEventHandlers {
attribute EventHandler onafterprint;
attribute EventHandler onbeforeprint;
attribute OnBeforeUnloadEventHandler onbeforeunload;
attribute EventHandler onhashchange;
attribute EventHandler onlanguagechange;
attribute EventHandler onmessage;
attribute EventHandler onmessageerror;
attribute EventHandler onoffline;
attribute EventHandler ononline;
attribute EventHandler onpagehide;
attribute EventHandler onpageshow;
attribute EventHandler onpopstate;
attribute EventHandler onrejectionhandled;
attribute EventHandler onstorage;
attribute EventHandler onunhandledrejection;
attribute EventHandler onunload;
};
一个mixin接口可以被一个或多个接口包含。除了HTMLBodyElement
,Window
接口、HTMLFrameSetElement
接口也包含了WindowEventHandlers
。试想一下,如果没有mixin接口这样的设计,那么这里所说的3个接口都需要在自己的IDL片段中添加这样一长串的事件属性,文档的内聚性就会变得很低,阅读体验也会变得很差。
mixin让你可以把接口进行组合,而partial则允许只展示接口的一部分。这有助于在解释接口的时候把读者的注意放在最关键的地方上。
比如这个例子:
partial interface Window {
undefined captureEvents();
undefined releaseEvents();
[Replaceable, SameObject] readonly attribute External external;
};
The captureEvents() and releaseEvents() methods must do nothing. // 解释接口
总结与延伸
短短两千字的文章,我没法给你做到对web IDL的全面覆盖。一些小的功能点,比如Dictionaries 、Typedefs 以及具体的数据类型我没有讲到,但是有了上面的基本框架,再去理解这些内容并不困难。除此以外,Web IDL还有很大的一部分篇幅是讲如何与ECMAScript绑定的,鉴于这是解读HTML标准的系列文章,所以就先不在这里讲了。
成员类型总结
成员类型 | 描述 | 格式 | 应用实例 |
---|---|---|---|
Constants | 声明一个实数 | const type constant_identifier = 42; | MediaError接口 使用Constants来义错误代码(code):const unsigned short MEDIA_ERR_ABORTED = 1; |
Regular Attribute | 常规属性 | readonly? attribute type identifier; | HTMLCollection 的length 属性。 |
Static Attributes | 静态属性 | static readonly? attribute type identifier; | HTML和DOM标准中没有有这个成员类型的接口 |
Stringifiers | 可以单独使用,也可以用在属性上,表示对象转化为string的结果 | stringifier; 或 stringifier attribute DOMString identifier; | Location接口 的href属性定义: stringifier attribute USVString href; ;所以 location.toString() === location.href // true |
Regular Operations | 常规方法 | return_type identifier(/* arguments... */); | DOMStringList接口定义了一个contains 方法:boolean contains(DOMString string); |
constructor operation | 构造器方法,声明这个成员表示可以通过构造器来创建实例。 | constructor(/* arguments... */) | Event接口 声明了一个construtor:constructor(DOMString type, optional EventInit eventInitDict = {}); |
Special Operations | 特殊方法。 特殊关键词有 getter 、 setter 、deleter | /* special_keyword */ return_type identifier?(/* arguments... */); | HTMLCollection的item 和nameItem 方法 |
Static Operations | 静态方法 | static return_type identifier(/* arguments... */); | HTMLScriptElement接口的supports方法是一个静态方法: static boolean supports(DOMString type); |
Iterable declarations | 表示这个接口可被遍历 | iterable<value_type>; 或iterable<key_type, value_type>; | NodeList接口有一个iterator: iterable<Node>; ;意味着可以用for..of.. 进行遍历。 |
Asynchronously iterable declarations | 异步遍历声明 | async iterable<value_type>; 或async iterable<key_type, value_type>; | HTML和DOM标准中没有有这个成员类型的接口 |
Maplike declarations | 具有map特性的成员 | maplike<key_type, value_type>; | HTML和DOM标准中没有有这个成员类型的接口 |
Setlike declarations | 具有set特性的成员 | setlike<type>; | HTML和DOM标准中没有有这个成员类型的接口 |