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 for
–in
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 Map
s:
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 Set
s 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 Map
s before placing its sub-classes into the structure.
classInheritanceTree
is only implicitly populated as classInheritanceReferences
is populated: the latter contains references to the Map
s 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.