0

Reading one of these MDN pages, I saw something like the SVG below where the object interface points to another one which it inherits from:

<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>

In general I noticed that they all inherit from EventTarget, then Node, and so on.

I am wondering: is there a full visualization of all these relationships? Possibly in a tree-like representation.

  • Quite unclear... in one hand you ask about web APIs and even link to a list of these APIs, and then you show only a list of DOM object interfaces. Which is it you are interested in? I mean, you realize no members of the first list is in the second, right? – Kaiido May 17 '21 at 22:56
  • I'm a beginner in this and I don't have a clear idea about their distinction. I ended up in the SVG from that list, so I assumed they're the same thing. I'm interested in the DOM object interfaces. I'll try to update the question. – jurgen_gjoncari May 18 '21 at 06:46
  • There's definitely a disconnect between your question and what you posted. Inheritance is a JavaScript construct. Yea, the DOM is JavaScript's representation of the page, but the way the DOM nodes are connected to each other have nothing to do with inheritance. Regarding tree visualization: HTML hierarchy is actually a tree. It's no coincidence that the DOM, a direct representation of the HTML, is also a tree. – Andrew May 18 '21 at 06:54
  • @Andrew referring again to the [MDN article](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement), there says: _it inherits properties by its parent element_. So, how is that not the same with JavaScript inheritance? – jurgen_gjoncari May 18 '21 at 07:09
  • @logicalclimber Where is that passage says "by its parent element"? I do not see it in the article. But I do see it state that the `HTMLTableCellElement` inherits from `HTMLElement`. The inheritance described is JavaScript object inheritance. – Andrew May 18 '21 at 08:33
  • @Andrew it says so directly under the [Properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement#properties) section. I still don't understand why an inheritance should be of a certain type. Especially in this case where object representation is almost identical - a dictionary type of properties and methods. – jurgen_gjoncari May 18 '21 at 09:15
  • @logicalclimber Your confusion stems from your assumption that the article is describing actual HTML. Again, it is JavaScript's representation of the HTML. `HTMLElement` is a JavaScript interface. It represents an "HTMLElement". `HTMLTableCellElement ` is the JavaScript representation of an HTML table cell. The definitions and diagrams are all describing JavaScript constructs. I don't know why it calls the object that it inherits from its "parent". That seems kind of misleading – Andrew May 18 '21 at 09:25
  • @Andrew, thanks for the explanation. So if this is essentially a JavaScript interface, then it means it is more similar to JavaScript object behaviour. And i'd expect them to be prototypes of other objects. This can also be seen in the console, the `HTMLElement.prototype`, in the end contains `__proto__: Element`, which in turn contains `__proto__: Node`, which makes sense with the representation of the SVG. So, my question was if it was possible to have a visualisation of all these default interfaces together. – jurgen_gjoncari May 18 '21 at 09:48
  • The diagram you're looking for is going to be pretty complex. For example, for inheritance down from `Node` [take a look at this](https://api.kde.org/3.5-api/kdelibs-apidocs/khtml/html/classDOM_1_1Node.html). – Jack Fleeting May 18 '21 at 10:35
  • @JackFleeting wow, thanks. That's exactly what I meant. – jurgen_gjoncari May 18 '21 at 11:20
  • Now we need someone to volunteer to complete the diagram up and sideways from `Node` :D – Jack Fleeting May 18 '21 at 11:35
  • @JackFleeting Yes, and if someone is planning to do it, please do something like a menu that can be collapsed – jurgen_gjoncari May 18 '21 at 13:32

1 Answers1

2

The tools

You can make this tree yourself by using a few JavaScript methods. In this approach, I’m going to use 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.

Getting the prototype

Inheritance in JavaScript works through the internal Prototype of an object. It can be observed with Object.getPrototypeOf. The prototype property of a derived constructor (function) is an instance of the base constructor (function); and its prototype property is the next step up the prototype chain.

These relationships clarify this:

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).

Note that constructors’ inheritance behavior can be misleading and is not what we’re going to be using:

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.

The prototype chain ends when trying to read the internal Prototype of an object reports null, which, in Web browsers, initially, only happens at Object.getPrototypeOf(Object.prototype). This works for all built-in and host-defined constructors, except Proxy, which doesn’t have a prototype property despite being a constructor. The reason it doesn’t (need to) have one is that a Proxy “instance” (i.e. new Proxy(target, handlers)) gets the Prototype of whatever the first argument (the Proxy target) is upon construction with new. We’ll leave it out for now.

Getting all classes

Getting all constructors is possible since most built-in and host-defined constructors are global, with the notable exception of TypedArray. Using Object.getOwnPropertyDescriptors yields all global properties and their descriptors. (On the web, window can be used instead of globalThis, on Node, it’s global.)

The descriptor contains some settings, e.g. if the property can be seen in a forin loop, etc. If the property is a getter / setter pair, you’ll see the corresponding get and set functions. Any normal property has a value descriptor. No constructor is a getter / setter pair, so a value must exist, and since all the constructors are global properties, we’re looking for functions. As mentioned earlier, these constructors must either have a prototype property, or be Proxy.

Object.entries(Object.getOwnPropertyDescriptors(globalThis))
  .filter(([_, {value}]) => value === Proxy || typeof value === "function" && value.hasOwnProperty("prototype"))

This gets a list of all constructors, but since Proxy is a special case and Object has a pesky “Null Prototype” to deal with, let’s actually filter them out and deal with them manually.

const allConstructors = Object.entries(Object.getOwnPropertyDescriptors(globalThis))
    .filter(([_, {value}]) => value !== Object && typeof value === "function" && value.hasOwnProperty("prototype"));

Generating the tree

We’ll initialize three Maps:

  • classInheritanceTree is the tree with the inheritance structure.
  • classInheritanceReferences is a flat structure mapping each constructor to a reference within classInheritanceTree.
  • constructorNames maps each constructor to any name associated with it.
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"
      ])
    ]
  ]);

Sure, null isn’t really part of the inheritance tree, but for visualization purposes it serves as a useful tree root. Note that .constructor.name doesn’t always match the property name on globalThis, e.g. in Firefox 90: webkitURL.name === "URL" and WebKitCSSMatrix.name === "DOMMatrix", and also webkitURL === URL and WebKitCSSMatrix === DOMMatrix. That’s why the values of constructorNames are Sets containing all aliases.

We populate all three maps simultaneously by iterating all constructors and determining the constructor of their prototypes. The self-call of the populateInheritanceTree function only ensures that a super-class exists in all Maps before placing its sub-classes into the structure. classInheritanceTree is only implicitly populated as classInheritanceReferences is populated: the latter contains references to the Maps inside the prior, so by updating one, we mutate the other as well.

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);
  }
});

Visualizing the tree

Once we have the classInheritanceTree, let’s put them into a <ul><li> structure. We’ll add a data-collapsed attribute to track which elements are expandable, and which of those are expanded and which are 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));

We sort the list items alphabetically, but list expandable ones first. The rest is just a bit of UI handling and CSS…

A tree of all constructors (except Proxy) exposed on the Web (normal browser context, not e.g. Worker)

This code puts all the previous steps together. Click on each expandable item to expand or collapse it. There’s also a picture of the result at the bottom.

However, I know you’ve asked about the Web APIs or the DOM APIs. These are difficult to isolate automatically, but hopefully that’s already helpful for now.

Exercise for the reader: automatically include links to MDN for each name in the tree.

"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;
}

This is how it looks like on my Firefox Nightly 90.0a1.

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
  • "all constructors available on the Web"... These are only the ones exposed in the Window context. Other contexts have access to other APIs (e.g WorkerLocation, PaintRenderingContext2D etc.) – Kaiido May 20 '21 at 04:23
  • @Kaiido Good point. I’ve fixed the subsection title. So that’s another exercise for the reader: visualize the tree for all constructors exposed in a Worker, and on Node.js. – Sebastian Simon May 20 '21 at 14:59
  • Thanks so much for this great answer! Would it be possible for you to save this in a github repository? This way if someone wants to add the links you mentioned, or do the same for Node, they can put everything in the same place. – jurgen_gjoncari Aug 06 '21 at 16:59
  • 1
    @logicalclimber Hm, I’ll consider it. Feel free to create a GitHub repo with this code yourself in the meantime. – Sebastian Simon Aug 06 '21 at 21:09