CSE 134B Web Client Languages
  1. Interaction
  2. JavaScript
  3. Prototypes and classes

Interaction · JavaScript 07

Prototypes and classes

JavaScript supports object-oriented programming, but the form it takes is disconcerting to anyone who learned it in a class style. An object inherits from another object, through a link that can be read and changed while the program runs. This page builds that model from constructors up, follows the chain from an object to null, and then shows that the class keyword added in 2015 is a syntax over the same mechanism.

This page assumes the objects and arrays page, including how this depends on how a function is called. Samples use a running example of robots. Predict each stated output before you run it, and use the Object walker in section 3 to look at the chains of actual objects, including a button from the DOM.

1. Constructors

Constructors are special functions that prepare new instances of an object for use. What makes one special is the way it is called. JavaScript has no separate constructor syntax for this older form, so an ordinary function becomes a constructor when you call it with new. Constructors are capitalized by convention, and the language does not check that convention.

// Constructors are capitalized by convention function Robot(needsToFly) { this.hasJetPack = needsToFly || false; } let guard = new Robot(true); let sidekick = new Robot(); console.log(guard.hasJetPack); // logs: true console.log(sidekick.hasJetPack); // logs: false

The || in that constructor gives the parameter a default. It works here because || returns one of its operands, a behavior the types page explains. new Robot() passes nothing, so needsToFly is undefined and the property becomes false.

What new does

The new operator does the work that the word "constructor" suggests. It takes four steps:

  1. It creates a blank object.
  2. It sets that object's prototype to the object in the function's prototype property. If that property does not hold an object, the new object gets Object.prototype instead.
  3. It runs the function with this bound to the new object, so this.hasJetPack = … adds a property to it.
  4. It gives you the new object, unless the function returned an object of its own, which then takes its place. A returned primitive is ignored.

Step 2 is the subject of the rest of this page. MDN sets out the same steps in its reference for the new operator.

Forgetting new

Nothing stops you from calling a constructor as a plain function, and in an ordinary script nothing complains. There is no new object, so this is the global object, and the constructor writes its properties onto that. The call returns undefined. The capital letter exists to warn a reader about this: a capitalized function called without new is probably a bug.

function Robot(needsToFly) { this.hasJetPack = needsToFly || false; } let guard = Robot(true); console.log(guard); // undefined console.log(globalThis.hasJetPack); // true, a new global

In strict mode and in modules, this is undefined in that call, so the assignment throws a TypeError and you find the bug at once. An arrow function cannot be used with new at all.

let Arrow = () => {}; console.log(Arrow.prototype); // logs: undefined new Arrow(); // throws: TypeError

One copy of every method per robot

Assigning to this gives each instance its own data, which is what you want for hasJetPack. It is less useful for behavior. A method written in the constructor is created again every time the constructor runs, so a thousand robots hold a thousand identical functions. In this playground, the two instances have different doAction functions, though the code is the same. Run it, then try the call without new.

A constructor also carries an object prototype that defines the code and data each instance has by default. Moving doAction there leaves one function, shared by every robot.

2. Prototypes

The prototype is a reference to an object describing the code and data that all objects of that type have in common. Two different things in the language are called "prototype":

  • Every object has an internal [[Prototype]]. It is a link to another object, or to null. It is not a property you can reach with a dot. You read it with Object.getPrototypeOf.
  • Functions have a .prototype property. It is an ordinary property that holds an ordinary object. Its one job is step 2 of new: it is the object that instances of that function get as their [[Prototype]].

So an instance does not have a .prototype, and the .prototype of a function is not that function's own prototype. A function's own [[Prototype]] is Function.prototype. The double brackets are how the specification writes an internal slot, and Chrome's DevTools uses the same notation.

function Robot() {} let guard = new Robot(); let plain = {}; console.log(Object.getPrototypeOf(guard) === Robot.prototype); // logs: true console.log(guard.prototype); // logs: undefined console.log(typeof Robot.prototype); // logs: object console.log(Object.getPrototypeOf(plain) === Object.prototype); // logs: true console.log(plain.prototype); // logs: undefined console.log(Object.getPrototypeOf(Robot) === Function.prototype); // logs: true console.log(Object.getPrototypeOf(Robot) === Robot.prototype); // logs: false
A constructor, its prototype object and two instancesThe function Robot has a property named prototype pointing at an object, Robot.prototype, which holds doAction and a constructor property pointing back at Robot. Two instances, guard and sidekick, each hold their own hasJetPack value, and each has an internal [[Prototype]] link to Robot.prototype. Neither instance links to the function itself.a functionfunction RobotRobot.prototypedoAction: functionconstructor: Robot.prototype.constructorguardhasJetPack: truesidekickhasJetPack: false[[Prototype]][[Prototype]]
Solid arrows are ordinary properties. Dashed arrows are the internal [[Prototype]] link that new sets. An instance links to the prototype object and has no link to the function.

Putting the shared parts on the prototype

This version moves the robot's defaults and its method to the prototype. The first two statements assign to Robot.prototype before the function Robot line, and they run because function declarations are hoisted, as the functions page explains. A robot gets an own hasJetPack only when it can fly. Otherwise a read of that name finds the default.

Robot.prototype.hasJetPack = false; Robot.prototype.doAction = function () { return 'Watch it!'; }; function Robot(flying) { if (flying === true) { this.hasJetPack = true; } } let guard = new Robot(true); let sidekick = new Robot(); let canFly = guard.hasJetPack; console.log(canFly); // logs: true console.log(sidekick.hasJetPack); // logs: false console.log(guard.doAction()); // logs: Watch it!

Reads walk, writes stay home

The model has one rule for reading and a different one for writing.

A read looks on the object first. If the name is not there, it follows [[Prototype]] and looks there, and so on upward. The first match wins. If the walk reaches null, the result is undefined.

A write creates or changes a property on the object itself and leaves the prototype alone. (The exceptions are a setter or a read-only property further up the chain.) So assigning sidekick.doAction gives that one robot its own method, which hides the shared one. This is called shadowing. Deleting the own property brings the shared one back into view, because the shared one was never touched.

Prototypes are shared by reference and consulted at the moment of the read, so a change to the prototype reaches every instance, including those created before the change. This playground shows all of it: one shared function, a change that reaches existing robots, an own property that shadows it, and a delete that undoes the shadow.

Other ways to set the link

A constructor is one way to set [[Prototype]]. Object.create does it directly, with no function involved, and it accepts null for an object with no prototype. An object literal can name its prototype with __proto__:, and that literal form is standard.

let robotParts = { doAction() { return 'Watch it!'; }, }; let guard = Object.create(robotParts); console.log(guard.doAction()); // logs: Watch it! console.log(Object.getPrototypeOf(guard) === robotParts); // logs: true let literal = { __proto__: robotParts }; console.log(literal.doAction()); // logs: Watch it! let bare = Object.create(null); console.log(Object.getPrototypeOf(bare)); // logs: null console.log(bare.toString); // logs: undefined

You will read obj.__proto__ in older code and tutorials. That accessor is deprecated and kept for web compatibility, so read the link with Object.getPrototypeOf. Object.setPrototypeOf can change the link after creation, but MDN warns that doing so disrupts the optimizations engines make to the chain, so set it when the object is created where you can.

3. Walking the chain

A prototype is an object, so it has a prototype too. Following the links gives the prototype chain, and every chain ends at null, which has no prototype. A literal is close to the end: its prototype is Object.prototype, whose prototype is null. A robot's chain has one more level, and toString comes from the top of it.

Four reads walking the chain from guardA vertical chain: guard, holding hasJetPack true; then Robot.prototype, holding hasJetPack false and doAction; then Object.prototype, holding toString and hasOwnProperty; then null. Reading guard.hasJetPack stops on guard and gives true, hiding the false on the prototype. Reading guard.doAction stops one level up. Reading guard.toString stops two levels up. Reading guard.fly reaches null and gives undefined.guardhasJetPack: trueRobot.prototypehasJetPack: falsedoAction: functionObject.prototypetoString, hasOwnProperty, …nullguard.hasJetPackstops here: trueguard.doActionstops one level upguard.toStringstops two levels upguard.flyreaches null: undefined
Each read starts at the object and climbs until the name appears. The hasJetPack on the prototype is never reached from guard, because the own property shadows it.

The Object walker

The same steps can be run against any object. Pick a starting object below, and the walker climbs with Object.getPrototypeOf, listing the own properties at each level until it reaches null. Type a name, or pick one, to see where a read of that name would stop and which levels further up it shadows. The name is only compared with property names as text, so nothing you type runs. The tool also has its own page.

Four things to look for as you try it:

  • An array's push is on Array.prototype, while its elements and length are its own. That is why every array can push without holding a copy of push.
  • toString on an array stops at Array.prototype, and the one on Object.prototype is shadowed. A built-in type overrides a general method in the same way a robot would.
  • A Date has no own properties at all. Its time is held in an internal slot that its methods read, so everything you can call is on Date.prototype. The built-ins page covers what those methods do.
  • A button also has no own properties until you add some. Its disabled is on HTMLButtonElement.prototype, click on HTMLElement.prototype, id on Element.prototype, textContent on Node.prototype and addEventListener on EventTarget.prototype, followed by the same Object.prototype and null that a literal has.

The DOM and events are described by a separate standard from the language, yet their inheritance is the same mechanism as the robot's. Many of those DOM properties show as accessors in the walker: disabled is a getter and setter pair on the prototype, which is why the walker reads descriptors and never reads the value. Reading HTMLButtonElement.prototype.disabled directly throws, because the prototype is not a button.

What walks and what stays on the object

Several tools answer the question "what properties does this have?", and they differ on whether they walk. in walks the chain. for/in walks it too, listing enumerable names, as the syntax page warned. Object.keys and Object.hasOwn stay on the object.

function Robot() { this.hasJetPack = true; } Robot.prototype.doAction = function () { return 'Watch it!'; }; let guard = new Robot(); console.log('doAction' in guard); // logs: true console.log(Object.hasOwn(guard, 'doAction')); // logs: false console.log(Object.keys(guard).join()); // logs: hasJetPack for (let key in guard) { console.log(key); } // logs: hasJetPack // logs: doAction

Primitives join in during a read. A string is not an object, but when you call a method on one, the language looks the method up on String.prototype, using a temporary wrapper that it then discards. Object.getPrototypeOf applied to a primitive gives the same answer. Writing a property on a string has nothing to keep it, so the value is gone by the next line.

let word = 'pear'; console.log(word.toUpperCase()); // logs: PEAR console.log(Object.getPrototypeOf(word) === String.prototype); // logs: true word.color = 'green'; console.log(word.color); // logs: undefined

4. Using prototypes on built-ins

Prototypes are of course shared, so you can do some interesting things. Built-in prototypes are ordinary objects that your script can write to, so a method you add to String.prototype is available on every string in the page, and you can invoke it as you would any other built-in method.

String.prototype.getThirdChar = function () { return this.charAt(2); }; // You can invoke this method as you would any other built-in method let c = 'Example'.getThirdChar(); console.log(c); // logs: a

There is a small oddity inside that method. In an ordinary script, a function called on a primitive string receives the wrapper object as this, so typeof this is 'object'. In strict mode it receives the primitive.

String.prototype.kind = function () { return typeof this; }; String.prototype.strictKind = function () { 'use strict'; return typeof this; }; console.log('x'.kind()); // logs: object console.log('x'.strictKind()); // logs: string

Why to be careful

Every script on the page uses the same String.prototype, including libraries, analytics and whatever arrives next year. MDN calls extending a built-in prototype monkey patching and a misfeature, for these reasons:

  • Names collide. Two scripts that add getThirdChar overwrite each other, and the last one to load wins for everybody, including code written against the other one.
  • The language keeps growing. If a later edition adds a method with your name and different behavior, one of the two breaks. The web has a record of this: the proposed Array.prototype.flatten broke a popular site in Firefox Nightly, probably through the MooTools library's own flatten. TC39 renamed the standard method flat so as not to break existing pages. Old patches can limit what the whole platform may name things.
  • Assigned properties are enumerable. A method added by assignment appears in every for/in over that type, which the built-in methods do not. The playground below shows it. Object.defineProperty avoids this, but not the other two problems.

MDN names one good reason to extend a built-in: a polyfill, which adds a standard method to an older engine that lacks it, with the standard's name and behavior. The how much JavaScript page returns to feature detection and polyfills. For your own helpers, write a function that takes the value, as in getThirdChar('Example'), or make a subclass, as section 7 shows.

There is a security version of this problem too. In prototype pollution, code writes to obj[key1][key2] with keys taken from outside the program. If key1 is "__proto__", the write lands on Object.prototype, and every ordinary object in the program inherits the attacker's value. MDN's defenses include objects with a null prototype, a Map for key and value data, and Object.hasOwn before a read that decides something.

Improving one instance

Sometimes you don't even need prototypes, especially if you just want to improve some built-in object instances. Assigning to an instance gives that one object an own property, and the shadowing rule from section 2 does the rest. This sample comes from the time when pages wrote their output with document.write, which the built-ins page covers as history.

document.writeln = function (s) { document.write(`${s}<br />\n`); }; document.writeln('hey there'); document.writeln('thomas powell');

A document already has a writeln, inherited from Document.prototype. The assignment does not change that one. It puts an own property on this one document object, which shadows the inherited method, so any other document, such as the one in an iframe, is unaffected. The same applies to any object.

let cart = ['pear', 'apple']; cart.last = function () { return this[this.length - 1]; }; let other = ['fig']; console.log(cart.last()); // logs: apple console.log(typeof other.last); // logs: undefined

A patch on one instance affects only the object you hold, so other arrays never see it. Overwriting a method that other scripts call on that same object, as the writeln sample does, still changes it for them.

5. Inheritance via the prototype chain

Inheritance in JavaScript is achieved through prototypes. To derive a new object type from a type that already exists, you arrange for instances to inherit all the properties of their own type in addition to any properties embodied in their parent. The way to do that with constructors is to make one type's prototype object inherit from the other's. This classic form still appears in older code.

function Robot(flying) { if (flying === true) { this.hasJetPack = true; } else { this.hasJetPack = false; } } Robot.prototype.doAction = function () { return 'Watch it!'; }; function UltraRobot(extraFeature) { if (extraFeature) { this.feature = extraFeature; } } UltraRobot.prototype = new Robot(); UltraRobot.prototype.feature = 'Radar'; let guard = new UltraRobot('Performs Calculus'); let feature = guard.feature; let canFly = guard.hasJetPack; console.log(feature); // logs: Performs Calculus console.log(canFly); // logs: false console.log(guard.doAction()); // logs: Watch it! console.log(new UltraRobot().feature); // logs: Radar

Walk each read. guard.feature stops on guard, because the constructor received a feature and assigned it. A second UltraRobot with no argument gets no own feature, so the read stops one level up and finds the default, 'Radar'. hasJetPack is also one level up, since that prototype object is itself a Robot. doAction is two levels up.

The chain of an UltraRobotA vertical chain: guard, with its own feature 'Performs Calculus'; then UltraRobot.prototype, which is an object made by new Robot, holding hasJetPack false and feature 'Radar'; then Robot.prototype, holding doAction; then Object.prototype; then null. Reading guard.feature stops on guard and shadows Radar. Reading guard.hasJetPack stops one level up. Reading guard.doAction stops two levels up.guardfeature: 'Performs Calculus'UltraRobot.prototype = new Robot()hasJetPack: falsefeature: 'Radar'Robot.prototypedoAction: function, constructor: RobotObject.prototypetoString, valueOf, …nullguard.featureown, shadows Radarguard.hasJetPackone level upguard.doAction()two levels up
The prototype of an UltraRobot is an actual robot, created once. Every UltraRobot inherits that one robot's hasJetPack.

What that pattern costs

The pattern has three problems. new Robot() runs the parent's constructor once, when the types are set up, with no arguments. So UltraRobot has no way to pass flying up, and each of its instances inherits the one hasJetPack: false. If the parent's constructor did something with side effects or required an argument, that would happen or fail at setup time. Replacing UltraRobot.prototype also discards the object that held the correct constructor, so guard.constructor finds the one on Robot.prototype and reports the wrong type.

The repair that became standard before class has three parts. Robot.call(this, flying) runs the parent's constructor on the new instance, so each instance gets its own data. Object.create(Robot.prototype) links the two prototypes without running a constructor. A final assignment puts constructor back. Compare the two in this playground.

instanceof in the last line of that sample is another chain walk. It asks whether the function's .prototype appears anywhere on the object's chain, so guard is an instance of UltraRobot, Robot and Object. Because the test looks at the chain as it stands now, changing a function's .prototype afterward can change its answer for objects that already exist.

6. Common properties and methods

Nearly every object ends its chain at Object.prototype, so nearly every object inherits a common set of members. The table lists them, with the internal link first. That link is often described as a prototype property on every object, but prototype is a property of functions.

MemberDescription
[[Prototype]]Internal link to the object from which this one inherits, read with Object.getPrototypeOf(obj). A function's prototype property is the separate object that becomes the [[Prototype]] of its instances.
constructorReference to the function associated with the prototype. It is inherited and writable, so it reports what the prototype says, and it can be wrong, as in section 5.
toString()Converts the object into a string (object-dependent behavior).
toLocaleString()Converts the object into a localized string (object-dependent behavior).
valueOf()Converts the object into an appropriate primitive where its type defines one, such as a number for a Date. The default on Object.prototype returns the object itself.
hasOwnProperty(prop)Returns true if the object has an own property named prop, and false otherwise. Prefer Object.hasOwn(obj, prop) in new code.
isPrototypeOf(obj)Returns true if this object appears anywhere on the chain of obj, not only as its immediate prototype.
propertyIsEnumerable(prop)Returns true if prop is an own property that is enumerable. An inherited enumerable property returns false, even though for/in lists it.
function Robot() { this.hasJetPack = true; } Robot.prototype.doAction = function () { return 'Watch it!'; }; let guard = new Robot(); console.log(guard.constructor === Robot); // logs: true console.log(guard.toString()); // logs: [object Object] console.log([1, 2].toString()); // logs: 1,2 console.log((1234.5).toLocaleString('en-US')); // logs: 1,234.5 console.log(new Date(0).valueOf()); // logs: 0 console.log(guard.valueOf() === guard); // logs: true console.log(guard.hasOwnProperty('hasJetPack')); // logs: true console.log(guard.hasOwnProperty('doAction')); // logs: false console.log(Robot.prototype.isPrototypeOf(guard)); // logs: true console.log(Object.prototype.isPrototypeOf(guard)); // logs: true console.log(guard.propertyIsEnumerable('hasJetPack')); // logs: true console.log(guard.propertyIsEnumerable('doAction')); // logs: false

Object.hasOwn has been available across browsers since March 2022 and is the recommended replacement for hasOwnProperty. The method form depends on the chain to exist and to be unmodified. An object with no prototype does not inherit it, and any object can define its own property with the same name.

let bare = Object.create(null); bare.name = 'Guard'; console.log(Object.hasOwn(bare, 'name')); // logs: true bare.hasOwnProperty('name'); // throws: TypeError

7. Objects the ES6 way

JavaScript supports OOP, but its form is disconcerting to those who learned in a "class" style. ES6, published in June 2015, rectifies this with a class style syntax, documented in MDN's classes reference. This is the first robot again.

class Robot { constructor(needsToFly) { if (needsToFly === true) { this.hasJetPack = true; } else { this.hasJetPack = false; } } } let guard = new Robot(true); console.log(JSON.stringify(guard)); // logs: {"hasJetPack":true} let sidekick = new Robot(); console.log(JSON.stringify(sidekick)); // logs: {"hasJetPack":false}

In a browser's console, console.log(guard) shows you the object itself, which you can expand to see its [[Prototype]]. The sample uses JSON.stringify so that its stated output is the same text everywhere.

The same model underneath

A class declares a constructor function and a prototype object in one statement. MDN says that classes do not bring a new inheritance pattern. You can check that directly. The name is a function, methods live on its .prototype, and instances link to that object. Adding to the prototype later still reaches instances that already exist. Choose "A class instance" in the walker and you will see doAction on Robot.prototype.

class Robot { doAction() { return 'Watch it!'; } } let guard = new Robot(); console.log(typeof Robot); // logs: function console.log(Object.getPrototypeOf(guard) === Robot.prototype); // logs: true console.log(Object.hasOwn(Robot.prototype, 'doAction')); // logs: true console.log(Object.hasOwn(guard, 'doAction')); // logs: false Robot.prototype.beep = function () { return 'Beep'; }; console.log(guard.beep()); // logs: Beep

What class changes

The syntax also closes off several of the traps in the sections above:

  • Calling without new throws a TypeError, so the silent global from section 1 cannot happen.
  • Class bodies are in strict mode, with or without a directive.
  • Methods are not enumerable, so they stay out of for/in. Compare the doAction that for/in listed in section 3.
  • A class cannot be used before its declaration runs. A function declaration is hoisted, which is what let the prototype assignments in section 2 come first. A class has the same temporal dead zone as let.
class Robot { constructor() { this.hasJetPack = true; } doAction() { return 'Watch it!'; } } let guard = new Robot(); for (let key in guard) { console.log(key); // logs: hasJetPack } class Robot {} Robot(); // throws: TypeError let guard = new Robot(); // throws: ReferenceError class Robot {}

Strict mode has one effect that catches people. A method pulled off its object loses its this, and in a class that this is undefined, so the call throws where sloppy code would have quietly used the global object. The objects page covers how a call sets this.

class Robot { constructor() { this.name = 'Guard'; } who() { return this.name; } } let guard = new Robot(); let who = guard.who; console.log(guard.who()); // logs: Guard who(); // throws: TypeError

extends and super

extends does the wiring that took three statements in section 5. It links the prototypes without running the parent's constructor at setup. super(…) runs the parent's constructor on the new instance, which is the job of Robot.call(this, …), and it must run before a derived constructor uses this. super.doAction() calls the method the current one shadows. constructor comes out right with no extra line. The class constructors are linked too, so a static method on Robot is inherited by UltraRobot.

Fields, accessors and private members

A public field such as step = 1 is defined as an own property of each instance, before the constructor body runs, or just after super() returns in a subclass. A get accessor lives on the prototype, which is the same arrangement as disabled on a button. A static member goes on the class rather than on instances. A name starting with # is private: reading it from outside the class body is a SyntaxError. The Chrome console relaxes this rule for debugging, so test it in a script.

class Counter { #count = 0; step = 1; increment() { this.#count = this.#count + this.step; } get total() { return this.#count; } } let counter = new Counter(); counter.increment(); counter.increment(); console.log(counter.total); // logs: 2 console.log(Object.keys(counter).join()); // logs: step console.log(Object.getOwnPropertyNames(Counter.prototype).join()); // logs: constructor,increment,total

Private members are the one place where the prototype model does not describe what you get. MDN describes them as metadata attached to each instance, outside prototypal inheritance, and no Object method can reach them. They are not properties, so the walker cannot show them, and that output shows no #count. A subclass cannot read its parent's private names either.

The safer way to extend a built-in

A subclass answers the problem in section 4. It gives you a new method on a type of your own, and Array.prototype stays as it was. The chain of a Stack runs through Stack.prototype to Array.prototype, so it remains an array in every way. The DOM uses the same arrangement: a custom element is a class that extends HTMLElement, as the components series shows.

class Stack extends Array { peek() { return this[this.length - 1]; } } let plates = new Stack(); plates.push('blue', 'green'); console.log(plates.peek()); // logs: green console.log(Array.isArray(plates)); // logs: true console.log(typeof [].peek); // logs: undefined

Transpile, need or not

When this syntax was new, the standing warning was to watch out for transpiling. Browsers in use did not all understand class, and Internet Explorer 11 never did, so a build step would rewrite classes as constructor functions and prototype assignments like those in section 5.

Treat that warning as history for class itself. MDN marks classes as Baseline widely available, across browsers since March 2016, and private members since July 2021. Microsoft ended support for the Internet Explorer 11 desktop app on most versions of Windows 10 in June 2022. For the newest syntax you meet, the question still applies. Check its Baseline status before adding a build step for it, as the how much JavaScript page describes.

8. Summary

ConceptKey points
ConstructorsAn ordinary function called with new, capitalized by convention. new makes an object, sets its prototype, runs the function with this as that object, and returns it.
[[Prototype]]Every object's internal link to another object or null. Read it with Object.getPrototypeOf. Avoid obj.__proto__.
.prototypeA property of functions. It holds the object that instances get as their [[Prototype]]. Instances do not have one.
Reads and writesReads walk the chain until the name appears, or reach null and give undefined. Writes go on the object itself and shadow anything further up.
SharingOne function on the prototype serves all instances, and a change to it reaches instances that already exist.
The chainEnds at null. A button walks through HTMLButtonElement, HTMLElement, Element, Node, EventTarget and Object by the same mechanism as a robot.
Extending built-insIt works and it is shared with every script on the page. Names collide, future standards collide, and assigned methods show in for/in. Polyfills are the accepted reason.
Own and inheritedin and for/in walk. Object.keys and Object.hasOwn do not. Prefer Object.hasOwn to hasOwnProperty.
Inheritance by handParent.call(this), Object.create(Parent.prototype), and restoring constructor. Child.prototype = new Parent() appears in old code and shares one parent's data.
classSyntax over the same model, with stricter rules: needs new, strict mode, methods not enumerable, unusable before its declaration. extends and super do the wiring. Private # members sit outside the prototype model.
Transpiling classA concern from the Internet Explorer years. Classes have run natively across browsers since 2016.