是否有所有 DOM 接口之间的继承关系的表示?
Posted
技术标签:
【中文标题】是否有所有 DOM 接口之间的继承关系的表示?【英文标题】:Is there a representation of the inheritance relationships between all the DOM interfaces? 【发布时间】:2021-05-17 21:02:57 【问题描述】:阅读these MDN pages 之一,我看到了类似下面的SVG,其中对象接口指向另一个它继承自的对象接口:
<svg style="display: inline-block; position: absolute; top: 0; left: 0;" viewBox="-50 0 600 120" preserveAspectRatio="xMinYMin meet"><a xlink:href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget" target="_top"><rect x="1" y="1" width="110" height="50" fill="#fff" stroke="#D4DDE4" stroke-width="2px"></rect><text x="56" y="30" font-size="12px" font-family="Consolas,Monaco,Andale Mono,monospace" fill="#4D4E53" text-anchor="middle" alignment-baseline="middle">EventTarget</text></a><polyline points="111,25 121,20 121,30 111,25" stroke="#D4DDE4" fill="none"></polyline><line x1="121" y1="25" x2="151" y2="25" stroke="#D4DDE4"></line><a xlink:href="https://developer.mozilla.org/en-US/docs/Web/API/Node" target="_top"><rect x="151" y="1" width="75" height="50" fill="#fff" stroke="#D4DDE4" stroke-width="2px"></rect><text x="188.5" y="30" font-size="12px" font-family="Consolas,Monaco,Andale Mono,monospace" fill="#4D4E53" text-anchor="middle" alignment-baseline="middle">Node</text></a><polyline points="226,25 236,20 236,30 226,25" stroke="#D4DDE4" fill="none"></polyline><line x1="236" y1="25" x2="266" y2="25" stroke="#D4DDE4"></line><a xlink:href="https://developer.mozilla.org/en-US/docs/Web/API/Element" target="_top"><rect x="266" y="1" width="75" height="50" fill="#fff" stroke="#D4DDE4" stroke-width="2px"></rect><text x="303.5" y="30" font-size="12px" font-family="Consolas,Monaco,Andale Mono,monospace" fill="#4D4E53" text-anchor="middle" alignment-baseline="middle">Element</text></a><polyline points="341,25 351,20 351,30 341,25" stroke="#D4DDE4" fill="none"></polyline><line x1="351" y1="25" x2="381" y2="25" stroke="#D4DDE4"></line><a xlink:href="https://developer.mozilla.org/en-US/docs/Web/API/htmlElement" target="_top"><rect x="381" y="1" width="110" height="50" fill="#fff" stroke="#D4DDE4" stroke-width="2px"></rect><text x="436" y="30" font-size="12px" font-family="Consolas,Monaco,Andale Mono,monospace" fill="#4D4E53" text-anchor="middle" alignment-baseline="middle">HTMLElement</text></a><polyline points="491,25 501,20 501,30 491,25" stroke="#D4DDE4" fill="none"></polyline><line x1="501" y1="25" x2="509" y2="25" stroke="#D4DDE4"></line><line x1="509" y1="25" x2="509" y2="90" stroke="#D4DDE4"></line><line x1="509" y1="90" x2="492" y2="90" stroke="#D4DDE4"></line><a xlink:href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement" target="_top"><rect x="291" y="65" width="200" height="50" fill="#F4F7F8" stroke="#D4DDE4" stroke-width="2px"></rect><text x="391" y="94" font-size="12px" font-family="Consolas,Monaco,Andale Mono,monospace" fill="#4D4E53" text-anchor="middle" alignment-baseline="middle">HTMLTableCellElement</text></a></svg>
总的来说,我注意到它们都继承自 EventTarget
,然后是 Node
,依此类推。
我想知道:是否有所有这些关系的完整可视化?可能以树状表示。
【问题讨论】:
很不清楚...一方面您询问 Web API,甚至链接到这些 API 的列表,然后您只显示 DOM 对象接口的列表。你对哪个感兴趣?我的意思是,你意识到第一个列表中没有成员在第二个列表中,对吧? 我是这方面的初学者,我不清楚它们的区别。我最终进入了该列表中的 SVG,所以我认为它们是相同的。我对 DOM 对象接口很感兴趣。我会尝试更新问题。 您的问题与您发布的内容之间肯定存在脱节。继承是一种 javascript 构造。是的,DOM 是 JavaScript 对页面的表示,但是 DOM 节点相互连接的方式与继承无关。关于树可视化:HTML 层次结构实际上是一棵树。 DOM 是 HTML 的直接表示,它也是一棵树,这绝非巧合。 @Andrew 再次提到MDN article,上面说:它通过其父元素继承属性。那么,这与 JavaScript 继承有何不同? @logicalclimber 这段话在哪里说“通过它的父元素”?我在文章中没有看到。但我确实看到它声明HTMLTableCellElement
继承自HTMLElement
。所描述的继承是 JavaScript 对象继承。
【参考方案1】:
工具
您可以使用一些 JavaScript 方法自己制作这棵树。
在这种方法中,我将大量使用Map
,因为它允许我们轻松地将任意值相互映射(即键不仅仅是对象中的字符串和符号)以及Set
。
获取原型
JavaScript 中的继承通过对象的内部原型进行。
可以通过Object.getPrototypeOf
观察。
派生构造函数(函数)的prototype
属性是基构造函数(函数)的一个实例;它的prototype
属性是原型链的下一步。
这些关系澄清了这一点:
Object.getPrototypeOf(Node.prototype) === EventTarget.prototype // A Node inherits properties from the EventTarget Prototype (EventTarget is the super-class of Node).
Object.getPrototypeOf(EventTarget.prototype) === Object.prototype // An EventTarget inherits properties from the Object Prototype (Object is the super-class of EventTarget).
Object.getPrototypeOf(Object.prototype) === null // An Object doesn’t inherit properties from anything (Object is a base class).
请注意,构造函数的继承行为可能会产生误导,这不是我们将要使用的:
Object.getPrototypeOf(Node) === EventTarget // This works, doesn’t it?
Object.getPrototypeOf(EventTarget) === Function.prototype // Function is _not_ the super-class of EventTarget; this is just the base-case for a constructor, which is a function.
Object.getPrototypeOf(Object) === Function.prototype // Again, Function is only shown here because the constructor is an instance of it.
当尝试读取对象的内部 Prototype 报告 null
时原型链结束,在 Web 浏览器中,最初只发生在 Object.getPrototypeOf(Object.prototype)
。
这适用于所有内置和主机定义的构造函数,除了 Proxy
,它没有 prototype
属性,尽管它是构造函数。
它没有(不需要)有一个的原因是代理“实例”(即new Proxy(target, handlers)
)在使用new
构造时获取第一个参数(代理目标)的原型。
我们暂时不考虑它。
获取所有类
获取所有构造函数是可能的,因为大多数内置和主机定义的构造函数都是全局的,TypedArray
除外。
使用Object.getOwnPropertyDescriptors
会产生所有全局属性及其描述符。
(在网络上,window
可以代替 globalThis
,在 Node 上是global
。)
描述符包含一些设置,例如如果可以在for
–in
循环等中看到该属性。
如果属性是 getter / setter 对,您将看到相应的 get
和 set
函数。
任何普通属性都有一个value
描述符。
没有构造函数是 getter / setter 对,因此必须存在 value
,并且由于所有 构造函数 都是全局属性,因此我们正在寻找 函数。
如前所述,这些构造函数必须要么具有prototype
属性,要么为Proxy
。
Object.entries(Object.getOwnPropertyDescriptors(globalThis))
.filter(([_, value]) => value === Proxy || typeof value === "function" && value.hasOwnProperty("prototype"))
这会得到所有构造函数的列表,但是由于Proxy
是一个特例,而Object
有一个讨厌的“Null Prototype”要处理,让我们实际过滤掉它们并手动处理它们。
const allConstructors = Object.entries(Object.getOwnPropertyDescriptors(globalThis))
.filter(([_, value]) => value !== Object && typeof value === "function" && value.hasOwnProperty("prototype"));
生成树
我们将初始化三个Map
s:
classInheritanceTree
是具有继承结构的树。
classInheritanceReferences
是一个平面结构,将每个构造函数映射到 classInheritanceTree
中的引用。
constructorNames
将每个构造函数映射到与其关联的任何名称。
const classInheritanceTree = new Map([
[
null,
new Map([
[
Object,
new Map()
]
])
],
]),
classInheritanceReferences = new Map([
[ null, classInheritanceTree.get(null) ],
[ Object, classInheritanceTree.get(null).get(Object) ]
]),
constructorNames = new Map([
[
null,
new Set([
"null"
])
],
[
Object,
new Set([
"Object"
])
]
]);
当然,null
并不是真正继承树的一部分,但出于可视化目的,它可用作有用的树根。
请注意,.constructor.name
并不总是与 globalThis
上的属性名称匹配,例如在 Firefox 90 中:webkitURL.name === "URL"
和 WebKitCSSMatrix.name === "DOMMatrix"
,还有 webkitURL === URL
和 WebKitCSSMatrix === DOMMatrix
。
这就是为什么constructorNames
的值是包含所有别名的Set
s。
我们通过迭代所有构造函数并确定其原型的constructor
同时填充所有三个映射。
populateInheritanceTree
函数的自调用仅确保在将其子类放入结构之前,所有Map
s 中都存在一个超类。
classInheritanceTree
仅在 classInheritanceReferences
被填充时隐式填充:后者包含对先验中 Map
s 的引用,因此通过更新一个,我们也改变了另一个。
allConstructors.forEach(function populateInheritanceTree([name, value])
const superClass = Object.getPrototypeOf(value.prototype).constructor;
// Make sure that the super-class is included in `classInheritanceReferences`;
// call function itself with parameters corresponding to the super-class.
if(!classInheritanceReferences.has(superClass))
populateInheritanceTree([
superClass.name,
value: superClass
]);
// If the class isn’t already included, place a reference into `classInheritanceReferences`
// and implicitly into `classInheritanceTree` (via `.get(superClass)`).
// Both Map values refer to the same Map reference: `subClasses`.
if(!classInheritanceReferences.has(value))
const subClasses = new Map();
classInheritanceReferences
.set(value, subClasses)
.get(superClass)
.set(value, subClasses);
// Create set for all names and aliases.
if(!constructorNames.has(value))
constructorNames.set(value, new Set());
// Add the property name.
constructorNames.get(value)
.add(name);
// Add the constructor’s `name` property if it exists (it may be different).
if(value.name)
constructorNames.get(value)
.add(value.name);
);
可视化树
一旦我们有了classInheritanceTree
,让我们将它们放入<ul>
–<li>
结构中。
我们将添加一个data-collapsed
属性来跟踪哪些元素是可展开的,哪些是展开的,哪些是折叠的。
const visualizeTree = (map, names) => Array.from(map)
.map(([constructor, subMap]) =>
const listItem = document.createElement("li"),
listItemLabel = document.createElement("span");
listItemLabel.append(...Array.from(names.get(constructor))
.flatMap((textContent) => [
Object.assign(document.createElement("code"),
textContent
),
", "
])
.slice(0, -1));
listItem.append(listItemLabel);
if(subMap.size)
const subList = document.createElement("ul");
listItem.setAttribute("data-collapsed", "false");
listItem.append(subList);
subList.append(...visualizeTree(subMap, names));
return listItem;
);
document.body.appendChild(document.createElement("ul"))
.append(...visualizeTree(classInheritanceTree, constructorNames));
我们按字母顺序对列表项进行排序,但首先列出可扩展的项。 剩下的只是一些 UI 处理和 CSS……
在 Web 上公开的所有构造函数(Proxy
除外)的树(普通浏览器上下文,而不是例如 Worker)
此代码将所有前面的步骤放在一起。 单击每个可展开项目以展开或折叠它。 底部还有一张结果图片。
不过,我知道您曾询问过 Web API 或 DOM API。 这些很难自动隔离,但希望现在已经有所帮助。
读者练习:自动为树中的每个名称包含指向 MDN 的链接。
"use strict";
const allConstructors = Object.entries(Object.getOwnPropertyDescriptors(globalThis))
.filter(([_, value]) => value !== Object && typeof value === "function" && value.hasOwnProperty("prototype")),
classInheritanceTree = new Map([
[
null,
new Map([
[
Object,
new Map()
]
])
]
]),
classInheritanceReferences = new Map([
[ null, classInheritanceTree.get(null) ],
[ Object, classInheritanceTree.get(null).get(Object) ]
]),
constructorNames = new Map([
[
null,
new Set([
"null"
])
],
[
Object,
new Set([
"Object"
])
]
]),
visualizeTree = (map, names) => Array.from(map)
.map(([constructor, subMap]) =>
const listItem = document.createElement("li"),
listItemLabel = document.createElement("span");
listItemLabel.append(...Array.from(names.get(constructor))
.flatMap((textContent) => [
Object.assign(document.createElement("code"),
textContent
),
", "
])
.slice(0, -1));
listItem.append(listItemLabel);
if(subMap.size)
const subList = document.createElement("ul");
listItem.setAttribute("data-collapsed", "false");
listItem.append(subList);
subList.append(...visualizeTree(subMap, names));
return listItem;
)
.sort((listItemA, listItemB) => listItemB.hasAttribute("data-collapsed") - listItemA.hasAttribute("data-collapsed") || listItemA.textContent.localeCompare(listItemB.textContent));
allConstructors.forEach(function populateInheritanceTree([name, value])
const superClass = Object.getPrototypeOf(value.prototype).constructor;
if(!classInheritanceReferences.has(superClass))
populateInheritanceTree([
superClass.name,
value: superClass
]);
if(!classInheritanceReferences.has(value))
const subClasses = new Map();
classInheritanceReferences
.set(value, subClasses)
.get(superClass)
.set(value, subClasses);
if(!constructorNames.has(value))
constructorNames.set(value, new Set());
constructorNames.get(value)
.add(name);
if(value.name)
constructorNames.get(value)
.add(value.name);
);
document.body.appendChild(document.createElement("ul"))
.append(...visualizeTree(classInheritanceTree, constructorNames));
addEventListener("click", (target) =>
if(target.closest("span") && target.closest("li").hasAttribute("data-collapsed"))
target.closest("li").setAttribute("data-collapsed", JSON.stringify(!JSON.parse(target.closest("li").getAttribute("data-collapsed"))));
);
ul
padding-left: 2em;
li
padding-left: .3em;
list-style-type: disc;
li[data-collapsed] > span
cursor: pointer;
li[data-collapsed] > span:hover
background: #ccc;
li[data-collapsed='false']
list-style-type: '▼';
li[data-collapsed='true']
list-style-type: '▶';
li[data-collapsed='true'] > ul
display: none;
这就是我的Firefox Nightly 90.0a1 上的样子。
【讨论】:
“Web 上可用的所有构造函数”... 这些只是在 Window 上下文中公开的构造函数。其他上下文可以访问其他 API(例如 WorkerLocation、PaintRenderingContext2D 等) @Kaiido 好点。我已经修复了小节标题。所以这是读者的另一个练习:可视化在 Worker 和 Node.js 上公开的所有构造函数的树。 ? 非常感谢这个很棒的答案!您可以将其保存在 github 存储库中吗?这样,如果有人想添加你提到的链接,或者对 Node 做同样的事情,他们可以把所有东西放在同一个地方。 @logicalclimber 嗯,我会考虑的。在此期间,您可以随意使用此代码创建一个 GitHub 存储库。以上是关于是否有所有 DOM 接口之间的继承关系的表示?的主要内容,如果未能解决你的问题,请参考以下文章