所有DOM接口之间的继承关系是否有表示?

Is there a representation of the inheritance relationships between all the DOM interfaces?

阅读其中一篇 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,依此类推。

我想知道:是否有所有这些关系的完整可视化?可能以树状表示。

工具

您可以使用一些 JavaScript 方法自己制作这棵树。 在这种方法中,我将使用 Map a lot, because it allows us to easily map arbitrary values to each other (i.e. keys are not just strings and symbols like in objects), along with 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.

尝试读取对象报告 null 的内部原型时原型链结束,在 Web 浏览器中,最初只发生在 Object.getPrototypeOf(Object.prototype)。 这适用于所有内置和主机定义的构造函数,除了 Proxy,它没有 prototype 属性 尽管是构造函数。 它不需要(需要)的原因是代理“实例”(即 new Proxy(target, handlers))在使用 new 构造时获取第一个参数(代理目标)的原型。 我们暂时不提它。

获取所有 classes

获取所有构造函数是可能的,因为大多数内置和主机定义的构造函数都是全局的,TypedArray 是一个明显的例外。 使用 Object.getOwnPropertyDescriptors 产生所有全局属性及其描述符。 (在 web 上,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 有一个讨厌的“空原型”需要处理,所以让我们实际过滤掉它们并处理它们手动。

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 的值是包含所有别名的 Set 的原因。

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

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。 这些很难自动隔离,但希望现在已经有用了。

reader 的练习:自动为树中的每个名称包含指向 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 上的样子。