你知道被标准化的 HTML 标签有多少个吗?

383 阅读5分钟

一、前言

前些天 winter 老师直播问了一个问题,你知道现在有多少个被标准化的 HTML 标签吗?这是个很有意思的问题。写了这么久的 HTML,那到底有多少个被标准化的 HTML标签呢?

二、获取 HTML 标签

首先,我们访问 HTML 标准文档,这张表格中整理了所有的 HTML 标签。我们可以将它们黏贴到编辑器中,看到总共有 109 行。如果你粗略的浏览下,你应该会发现,h1h2一直到 h6 都是放在一起的。所以目前标准化的所有标签是 114 个。

到这里,我们的查询任务已经完成了。但是我们就止于此吗?不。

三、HTML 标签是怎么对应其 DOM 接口的?

思路是这样的:我们先从规范中整理出所有标签对应的接口,然后再从 window 对象中获取到所有 DOM 接口,最后再将它们进行比较,看看规范与具体浏览器实现的对应关系。

(1)从规范中查询所有定义的接口列表

规范中 这样定义 HTML 中的 elements

The nodes representing HTML elements in the DOM must implement, and expose to scripts, the interfaces listed for them in the relevant sections of this specification.

意思是说, DOM 中 HTML 元素的节点必须实现规范相关部分中列出的接口,并将这些接口公开给脚本。也就是说,所有的 HTML 标签都会有对应的 DOM 接口。因此,我们根据前面获取到的标签表格依次去查询标签描述就可以得出所有的接口列表。

规范在描述所有标签时给出了对应的 IDL(Interface description language) 。比如,打开一个 span 标签查看它 DOM interface 的描述:

[Exposed=Window]
interface HTMLSpanElement : HTMLElement {
  [HTMLConstructor] constructor();
};

可以看到 span 标签的 DOM 接口是 HTMLSpanElement,并且继承自 HTMLElement 接口。

规范规定,HTMLElement 接口是一个基本接口,所有 HTML 元素的接口都从该接口继承。

最终得到的接口列表如下:

// 对应已知的 DOM interface,除 HTMLElement 外 63 个。
const preList = [
                // multi element 6
                // 'HTMLElement',
                'HTMLQuoteElement',
                'HTMLTableSectionElement',
                'HTMLTableCellElement',
                'HTMLTableColElement',
                'HTMLHeadingElement',
                'HTMLModElement',
                // root element 1
                'HTMLHtmlElement',
                // metadata 8
                'HTMLBaseElement',
                'HTMLHeadElement',
                'HTMLLinkElement',
                'HTMLMetaElement',
                'HTMLCanvasElement',
                'HTMLStyleElement',
                'HTMLTitleElement',
                'HTMLScriptElement',
                // inline-text-semantics 5
                'HTMLAnchorElement',
                'HTMLBRElement',
                'HTMLDataElement',
                'HTMLSpanElement',
                'HTMLTimeElement',
                // forms 14
                'HTMLButtonElement',
                'HTMLDataListElement',
                'HTMLFieldSetElement',
                'HTMLFormElement',
                'HTMLInputElement',
                'HTMLLabelElement',
                'HTMLLegendElement',
                'HTMLMeterElement',
                'HTMLOptGroupElement',
                'HTMLOptionElement',
                'HTMLOutputElement',
                'HTMLProgressElement',
                'HTMLSelectElement',
                'HTMLTextAreaElement',
                // text-content 8
                'HTMLDivElement',
                'HTMLDListElement',
                'HTMLHRElement',
                'HTMLLIElement',
                'HTMLOListElement',
                'HTMLParagraphElement',
                'HTMLPreElement',
                'HTMLUListElement',
                // content-sectioning-root 1
                'HTMLBodyElement',
                // interactive-element 3
                'HTMLDetailsElement',
                'HTMLDialogElement',
                'HTMLMenuElement',
                // tabular-data 3
                'HTMLTableCaptionElement',
                'HTMLTableElement',
                'HTMLTableRowElement',
                // web-components 2
                'HTMLSlotElement',
                'HTMLTemplateElement',
                // image-multimedia-embedded-content 12
                'HTMLAreaElement',
                'HTMLAudioElement',
                'HTMLImageElement',
                'HTMLMapElement',
                'HTMLTrackElement',
                'HTMLVideoElement',
                'HTMLEmbedElement',
                'HTMLIFrameElement',
                'HTMLObjectElement',
                'HTMLParamElement',
                'HTMLPictureElement',
                'HTMLSourceElement'
            ];

(2)从浏览器中获取所有接口列表

从上面的定义中注意到,规范规定所有的接口都会暴露给脚本。这样我们就可以从 window 上获取到所有属性,然后从中过滤出来所有继承自 HTMLElement 接口的对象。获得所有接口后再进行转换一下打印出来。

// 获取 window 上的继承自 HTMLElement 的 interface
const windowIntList = [...new Set(Object.getOwnPropertyNames(window)
                     .map(name => window[name] && window[name].prototype)
                     .filter(o => o instanceof HTMLElement)
                     .map(name => {
                     	return Object.prototype.toString.call(name).replace(/\[object|\]| /g, ''); 
                     }))];
                     
// output:
// Array(72)
// 0: "HTMLOptionElement"
// 1: "HTMLImageElement"
// 2: "HTMLAudioElement"
// 3: "HTMLVideoElement"
// 4: "HTMLUnknownElement"
// 5: "HTMLUListElement"
// 6: "HTMLTrackElement"
// 7: "HTMLTitleElement"
// 8: "HTMLTimeElement"
// 9: "HTMLTextAreaElement"
// 10: "HTMLTemplateElement"
// 11: "HTMLTableSectionElement"
// 12: "HTMLTableRowElement"
// 13: "HTMLTableElement"
// 14: "HTMLTableColElement"
// 15: "HTMLTableCellElement"
// 16: "HTMLTableCaptionElement"
// 17: "HTMLStyleElement"
// 18: "HTMLSpanElement"
// 19: "HTMLSourceElement"
// 20: "HTMLSlotElement"
// 21: "HTMLShadowElement"
// 22: "HTMLSelectElement"
// 23: "HTMLScriptElement"
// 24: "HTMLQuoteElement"
// 25: "HTMLProgressElement"
// 26: "HTMLPreElement"
// 27: "HTMLPictureElement"
// 28: "HTMLParamElement"
// 29: "HTMLParagraphElement"
// 30: "HTMLOutputElement"
// 31: "HTMLOptGroupElement"
// 32: "HTMLObjectElement"
// 33: "HTMLOListElement"
// 34: "HTMLModElement"
// 35: "HTMLMeterElement"
// 36: "HTMLMetaElement"
// 37: "HTMLMenuElement"
// 38: "HTMLMediaElement"
// 39: "HTMLMarqueeElement"
// 40: "HTMLMapElement"
// 41: "HTMLLinkElement"
// 42: "HTMLLegendElement"
// 43: "HTMLLabelElement"
// 44: "HTMLLIElement"
// 45: "HTMLInputElement"
// 46: "HTMLIFrameElement"
// 47: "HTMLHtmlElement"
// 48: "HTMLHeadingElement"
// 49: "HTMLHeadElement"
// 50: "HTMLHRElement"
// 51: "HTMLFrameSetElement"
// 52: "HTMLFrameElement"
// 53: "HTMLFormElement"
// 54: "HTMLFontElement"
// 55: "HTMLFieldSetElement"
// 56: "HTMLEmbedElement"
// 57: "HTMLDivElement"
// 58: "HTMLDirectoryElement"
// 59: "HTMLDialogElement"
// 60: "HTMLDetailsElement"
// 61: "HTMLDataListElement"
// 62: "HTMLDataElement"
// 63: "HTMLDListElement"
// 64: "HTMLContentElement"
// 65: "HTMLCanvasElement"
// 66: "HTMLButtonElement"
// 67: "HTMLBodyElement"
// 68: "HTMLBaseElement"
// 69: "HTMLBRElement"
// 70: "HTMLAreaElement"
// 71: "HTMLAnchorElement"
// length: 72

经过以上操作获得 72 个继承自 HTMLElement 接口的接口。

(3)preListwindowIntList 的比较

我们得到的 preListwindowIntList 各自的个数为 63 和 72。但是前面算出来的标签确是 114 个。不是每个标签都有对应的接口吗?难道这三个算出来的个数不应该是一样的吗?

如果你也自己去整理了 preList,那你一定知道 其中不仅有很多标签具有相同接口,而且还有大量标签的接口都是基础接口 HTMLElement。如果你没有整理也没有关系,下面是我整理的对应关系:

接口个数标签
HTMLQuoteElement2qblockquote
HTMLTableSection3theadtbodytfoot
HTMLTableCell2tdth
HTMLTableCol2colcolgroup
HTMLHeadingElement6h1h2h3h4h5h6
HTMLModElement2delins
HTMLElement40addressarticleasidefooterheaderhgroupnavsectionmainsummarynoscriptabbrbbdibdocitecodedfnemikbdmarkrbrprtrtcrubyssmapsmallstrongsubsupuvarwbrdddtfigurefigcaption

由此我们可以得出 preList 的总个数为 63 加上 HTMLElement 接口的个数再加上其它接口重复的个数:63 + 40 + 1 + 2 + 1 + 1 + 5 + 1 = 114 个。欧克,对上了,完美。:)

通过比较, preList 中所有的元素都在 windowIntList 中。windowIntList 中则比 preList 多出了这些接口:

// HTMLUnknownElement
// HTMLShadowElement
// HTMLMediaElement
// HTMLMarqueeElement
// HTMLFrameSetElement
// HTMLFrameElement
// HTMLFontElement
// HTMLDirectoryElement
// HTMLContentElement

又经过一番翻天覆地的查找,发现上述列表除了 HTMLMediaElementHTMLUnknownElement 其它的都是已经废弃了的接口。HTMLMediaElement 接口则是其它多媒体的底层接口。它们的继承关系是这样的:

多媒体接口

至于 HTMLUnknownElement 接口,它代表着一个无效的 HTML 元素,继承自 HTMLElement 接口。当我们使用一个无效的标签时,仍然可以对其操作及样式设置。

<!DOCTYPE html>
<html>
    <style>
        test {
            color: red;
        }
    </style>
    <body>
        <test>nihao</test>
    </body>
    <script>
        {
            const dom = document.getElementsByTagName('test')[0];
            console.log(dom instanceof HTMLUnknownElement)
            // output: true
        }
    </script>
</html>


四、最后

很可能当你阅读到这篇文章时被标准化的 HTML 已经不是这个数字了。但是没关系,希望这篇文章能为你提供思路。最后,有空的话,一定要自己去动手操作一下,这样可以理解的更深刻哦~


推荐一个网站:在线查阅 HTML 标签

系统信息:
OS:macOS Catalina 10.15.7
Browser Version: Chrome 87.0.4280.88(正式版本) (x86_64)