Prototypes and inheritance

See the prototype chain that every object lookup follows, and what class and extends really build.

  • Beginner
  • 10 min read
  • Updated 17 Oct 2026

What you will learn

  • Trace a property through the prototype chain
  • Explain what class syntax does
  • Create objects with a chosen prototype using Object.create
  • Tell own properties from inherited ones

Before you start

  • Classes
  • Classes, extends and super
  • The this keyword

An empty array has a map method. A plain object has toString. An instance of your Course class has describe, even though you never put it there. All of these come from prototypes: every object can point to another object, and when a property is missing, JavaScript looks there instead. Classes are built on this mechanism, so understanding it explains how class, extends, instanceof and many puzzling bugs actually work.

Every object has a prototype

Each object has a hidden internal link, written in the specification as [[Prototype]], to another object or to null. You read it with Object.getPrototypeOf():

JavaScript
const course = { title: "JavaScript Basics" };

console.log(Object.getPrototypeOf(course) === Object.prototype); // → true
console.log(Object.getPrototypeOf(Object.prototype));            // → null

An object literal's prototype is Object.prototype, the object that provides toString, hasOwnProperty and the other methods every object seems to have. Object.prototype itself has no prototype, so the chain ends there.

How property lookup works

When you read obj.prop, the engine:

  1. Checks whether obj has an own property called prop. If so, it uses it.
  2. If not, it moves to obj's prototype and checks there.
  3. It repeats up the chain until it finds the property or reaches null, in which case the result is undefined.
JavaScript
console.log(course.title);    // → JavaScript Basics (own property)
console.log(typeof course.toString); // → function (found on Object.prototype)
console.log(course.missing);  // → undefined (not found anywhere)

Arrays work the same way, with one more step:

Plain text
[1, 2, 3]  ──>  Array.prototype  ──>  Object.prototype  ──>  null
 (own: 0, 1, 2, length)  (map, filter, push...)  (toString, hasOwnProperty...)
JavaScript
const numbers = [1, 2, 3];

console.log(Object.getPrototypeOf(numbers) === Array.prototype);                   // → true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype);          // → true
console.log(Object.hasOwn(numbers, "map"));         // → false
console.log(Object.hasOwn(Array.prototype, "map")); // → true

Every array shares one map function stored on Array.prototype. That is the whole point of prototypes: shared behaviour lives in one place, and each object stores only its own data.

Object.create: choosing a prototype

Object.create(proto) makes a new, empty object whose prototype is proto. It is the most direct way to see the chain in action:

JavaScript
const courseMethods = {
  describe() {
    return `${this.title}: ${this.lessons} lessons`;
  },
};

const js = Object.create(courseMethods);
js.title = "JavaScript Basics";
js.lessons = 12;

console.log(js.describe());                                // → JavaScript Basics: 12 lessons
console.log(Object.getPrototypeOf(js) === courseMethods); // → true
console.log(Object.keys(js));                              // → [ 'title', 'lessons' ]

describe is found on courseMethods, one step up the chain. Inside it, this is still js, because this is set by how the method is called (js.describe()), not by where it was found. That rule, from lesson 21, is what lets one shared method work for every object.

Chains can be as long as you like:

JavaScript
const base = { greet() { return "hello"; } };
const middle = Object.create(base);
const leaf = Object.create(middle);

console.log(leaf.greet()); // → hello (found two steps up)

Objects with no prototype

Object.create(null) makes an object with no prototype at all. It has no toString, no hasOwnProperty, nothing:

JavaScript
const lookup = Object.create(null);
lookup.constructor = "a key like any other";

console.log("toString" in lookup); // → false

This used to be the standard way to build a safe dictionary from untrusted keys. Today a Map (lesson 37) is usually the better choice.

Own and inherited properties

Many bugs come from mixing up properties an object has itself with ones it inherits.

CheckIncludes inherited properties?
Object.hasOwn(obj, key)No, own properties only
key in objYes
Object.keys(obj), Object.entries(obj)No, own enumerable properties only
for...inYes, own and inherited enumerable properties
obj.key !== undefinedYes, and it cannot tell "missing" from "set to undefined"
JavaScript
console.log("describe" in js);           // → true
console.log(Object.hasOwn(js, "describe")); // → false

Object.hasOwn() was added in ES2022 and is supported in all current browsers and Node.js. It replaces obj.hasOwnProperty(key), which fails on objects created with Object.create(null) and can be overwritten by a property of the same name.

Writing does not go up the chain

Reading walks the chain; writing does not. Assigning to a property creates or updates an own property on the object, even if the prototype has a property with that name:

JavaScript
const defaults = { theme: "light" };
const userSettings = Object.create(defaults);

console.log(userSettings.theme); // → light (inherited)

userSettings.theme = "dark";     // creates an own property
console.log(userSettings.theme); // → dark (own property shadows the inherited one)
console.log(defaults.theme);     // → light (unchanged)

delete userSettings.theme;
console.log(userSettings.theme); // → light (inherited one visible again)

The own property shadows the inherited one. Two exceptions: if the prototype defines a setter for that name, the setter runs instead; and if the inherited property is read-only, the assignment fails (silently in sloppy code, with a TypeError in strict code).

One trap follows from this. Mutating an inherited object or array does not create an own property; it changes the shared one:

JavaScript
const shared = { tags: [] };
const a = Object.create(shared);
const b = Object.create(shared);

a.tags.push("css");     // reads tags (inherited), then mutates that one array
console.log(b.tags);    // → [ 'css' ]

This is why data belongs on each instance and only methods belong on the prototype.

Constructor functions: the original pattern

Before class existed, objects with shared methods were built with constructor functions. Every ordinary function has a prototype property, an object that becomes the prototype of anything created with new:

JavaScript
function Course(title, lessons) {
  this.title = title;
  this.lessons = lessons;
}

Course.prototype.describe = function () {
  return `${this.title}: ${this.lessons} lessons`;
};

const css = new Course("CSS Grid in Practice", 8);
console.log(css.describe()); // → CSS Grid in Practice: 8 lessons
console.log(Object.getPrototypeOf(css) === Course.prototype); // → true

new Course(...) does four things:

  1. Creates a new empty object.
  2. Sets its prototype to Course.prototype.
  3. Calls Course with this set to the new object.
  4. Returns the new object (unless the function returns a different object itself).

Do not confuse the two "prototypes". Course.prototype is an ordinary property of the function, holding the object that instances will inherit from. It is not the prototype of Course itself, which is Function.prototype.

You will still find this pattern in older code and libraries.

What class really builds

Here is the Course class from lesson 36:

JavaScript
class Course {
  constructor(title, lessons) {
    this.title = title;
    this.lessons = lessons;
  }

  describe() {
    return `${this.title}: ${this.lessons} lessons`;
  }

  static fromJSON(data) {
    return new Course(data.title, data.lessons);
  }
}

console.log(typeof Course);                                   // → function
console.log(Object.hasOwn(Course.prototype, "describe"));     // → true
console.log(Object.hasOwn(Course, "fromJSON"));               // → true

The class produced the same structure as the constructor function: a function called Course, instance methods on Course.prototype, and static methods as properties of the function itself. Class syntax is cleaner, and it adds some real differences:

  • A class must be called with new; calling it as a normal function throws a TypeError.
  • Methods are non-enumerable, so they do not show up in for...in loops over instances.
  • The class body is always in strict mode, and the class is in the temporal dead zone until its declaration runs.
  • Private #fields have no equivalent in the constructor function pattern.

What extends builds

extends links two chains:

JavaScript
class VideoCourse extends Course {
  constructor(title, lessons, minutes) {
    super(title, lessons);
    this.minutes = minutes;
  }
}

const video = new VideoCourse("Flexbox Fundamentals", 6, 95);

// Instances: video -> VideoCourse.prototype -> Course.prototype -> Object.prototype
console.log(Object.getPrototypeOf(VideoCourse.prototype) === Course.prototype); // → true

// Statics: VideoCourse -> Course, so static methods are inherited too
console.log(Object.getPrototypeOf(VideoCourse) === Course); // → true
console.log(typeof VideoCourse.fromJSON);                   // → function

Tracing video.describe():

  1. video has own properties title, lessons and minutes. No describe.
  2. VideoCourse.prototype has constructor and nothing else. No describe.
  3. Course.prototype has describe. Found; call it with this set to video.

super.describe() inside a VideoCourse method simply starts the lookup at Course.prototype instead of at the object.

instanceof follows the chain

obj instanceof Fn asks: does Fn.prototype appear anywhere in obj's prototype chain?

JavaScript
console.log(video instanceof VideoCourse); // → true
console.log(video instanceof Course);      // → true
console.log(video instanceof Object);      // → true
console.log(Object.create(null) instanceof Object); // → false

That is why an error created by your HttpError class from lesson 35 is also instanceof Error. It also explains a surprise: objects from another window or iframe have a different Array.prototype, so instanceof Array is false for them. Array.isArray() works across windows, which is why it exists.

Things to avoid

Changing built-in prototypes

You can add methods to Array.prototype or String.prototype, and every array or string in the page gets them. Do not. If a future version of JavaScript adds a method with the same name, your version and the standard one collide. This has happened: a widely used library had added its own Array.prototype.flatten, and the standard method had to be named flat instead to avoid breaking sites. Write a plain function instead.

Changing an object's prototype after creation

Object.setPrototypeOf(obj, proto) exists, but changing the prototype of an existing object makes engines discard optimisations for it and for code that touches it. Set the prototype when you create the object, with Object.create or a class. Lesson 51 explains why engines care.

The legacy __proto__ accessor

Older code reads and writes obj.__proto__. It is kept only for compatibility. Use Object.getPrototypeOf() and Object.create() instead.

Prototype pollution is a real class of security bug. Code that deep-merges untrusted JSON into an object, following keys blindly, can be tricked by a key named __proto__ into writing to Object.prototype, which changes every object in the program. When copying keys from untrusted data, skip __proto__, constructor and prototype, or store the data in a Map.

Summary

  • Every object has a [[Prototype]] link to another object or null; read it with Object.getPrototypeOf().
  • Reading a property walks the chain until it is found or the chain ends. Writing creates an own property that shadows the inherited one.
  • Object.create(proto) creates an object with a chosen prototype; Object.create(null) creates one with none.
  • Object.hasOwn() and Object.keys() see own properties; in and for...in also see inherited ones.
  • new Fn() creates an object whose prototype is Fn.prototype, then runs Fn with this set to it.
  • class puts methods on Class.prototype and statics on the class; extends links both the prototype chain and the static chain.
  • instanceof checks whether a prototype appears in an object's chain.
  • Keep data on instances and methods on prototypes, never modify built-in prototypes, and guard against prototype pollution.

Further reading