是否有所有 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。)

描述符包含一些设置,例如如果可以在forin 循环等中看到该属性。 如果属性是 getter / setter 对,您将看到相应的 getset 函数。 任何普通属性都有一个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"));

生成树

我们将初始化三个Maps:

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 === URLWebKitCSSMatrix === DOMMatrix。 这就是为什么constructorNames 的值是包含所有别名的Sets。

我们通过迭代所有构造函数并确定其原型的constructor 同时填充所有三个映射。 populateInheritanceTree 函数的自调用仅确保在将其子类放入结构之前,所有Maps 中都存在一个超类。 classInheritanceTree 仅在 classInheritanceReferences 被填充时隐式填充:后者包含对先验中 Maps 的引用,因此通过更新一个,我们也改变了另一个。

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 接口之间的继承关系的表示?的主要内容,如果未能解决你的问题,请参考以下文章

接口、继承以及它们之间的关系

interface Part2(定义接口)

Spring04-----DI

UML中的关系及表示

Spring学习:DI的配置

[原创]抽象类实现接口,子类继承抽象类,这三者之间的关系?