Skip to content
Educora
Advanced20 min16 / 27

Prototypes and inheritance

See what really happens behind `class`: the prototype chain, `Object.create`, constructor functions and why classes are called syntactic sugar.

Check yourself
In this lesson you will learn
  • Explain how a property lookup walks the prototype chain up to Object.prototype and null
  • Create objects with Object.create and with constructor functions called with new
  • Show that class is built on prototypes and tell own properties from inherited ones

Every array has a map method and every string has toUpperCase — yet you never wrote these methods into your arrays. Where do they come from? In JavaScript every object has a hidden link to another object — its prototype. When a property is not found on the object itself, JavaScript looks at the prototype. Built-in methods, inheritance and classes are all built on this simple mechanism.

The prototype chain

You can read an object's prototype with Object.getPrototypeOf(obj) (the old __proto__ accessor also works but is not recommended). When obj.x is read, JavaScript first checks the object's own properties, then its prototype, then the prototype's prototype and so on — until it reaches null. If the property is nowhere, the result is undefined. Writing works differently: obj.x = ... always creates or changes an own property and never touches the prototype.

JavaScript
const animal = {
  eats: true,
  describe() {
    return `${this.name} eats: ${this.eats}`;
  },
};

const cat = Object.create(animal);
cat.name = 'Tom';

console.log(cat.describe());
console.log(Object.getPrototypeOf(cat) === animal);
console.log(Object.hasOwn(cat, 'name'), Object.hasOwn(cat, 'eats'));

cat.eats = false;
console.log(cat.eats, animal.eats);

console.log(Object.getPrototypeOf(animal) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));
▸ Expected output
Tom eats: true
true
true false
false true
true
null
cat.eats = false created an own property that “shadows” the inherited one — animal did not change. The chain: cat → animal → Object.prototype → null.

Built-in objects work the same way. An array's chain is: the array → Array.prototype → Object.prototype → null. map lives not in the array itself but in Array.prototype. The in operator checks the whole chain, while Object.hasOwn checks only own properties. Array.prototype has its own toString, which shadows Object.prototype.toString.

JavaScript
const numbers = [1, 2, 3];

console.log(Object.getPrototypeOf(numbers) === Array.prototype);
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype);
console.log(Object.hasOwn(numbers, 'map'), 'map' in numbers);
console.log(numbers.toString === Object.prototype.toString);
console.log(numbers.toString());
▸ Expected output
true
true
false true
false
1,2,3

Constructor functions and new

Before class existed, objects with the same shape were created with constructor functions. When you call any regular function with new, JavaScript takes four steps:

  1. creates an empty object;
  2. sets its prototype to Fn.prototype;
  3. runs the function with this set to the new object;
  4. returns the new object, unless the function returns another object.

Methods are put on Fn.prototype: then all instances share one copy of each method instead of every object carrying its own. In a program with a thousand objects this saves real memory.

JavaScript
function Student(name, grade) {
  this.name = name;
  this.grade = grade;
}

Student.prototype.introduce = function () {
  return `${this.name}, grade ${this.grade}`;
};

const leyla = new Student('Leyla', 9);
const elvin = new Student('Elvin', 10);

console.log(leyla.introduce(), '|', elvin.introduce());
console.log(leyla.introduce === elvin.introduce);
console.log(Object.getPrototypeOf(leyla) === Student.prototype);
console.log(leyla);
▸ Expected output
Leyla, grade 9 | Elvin, grade 10
true
true
Student { name: 'Leyla', grade: 9 }

Classes are syntactic sugar

class (ES2015) lets you write the same structure more readably: a class is really a function, its methods land on ClassName.prototype, and extends links the prototypes together. There are differences too: class code is always in strict mode, a class cannot be called without new, methods don't show up in for...in, and #private fields exist only in classes.

JavaScript
class Shape {
  constructor(name) { this.name = name; }
  describe() { return `I am a ${this.name}`; }
}

class Circle extends Shape {
  constructor(radius) {
    super('circle');
    this.radius = radius;
  }
  area() { return +(Math.PI * this.radius ** 2).toFixed(2); }
}

const c = new Circle(2);
console.log(typeof Shape, c.describe(), c.area());
console.log(Object.getPrototypeOf(Circle.prototype) === Shape.prototype);
console.log(Object.getOwnPropertyNames(Circle.prototype));
console.log(Object.hasOwn(c, 'area'), 'area' in c);

try {
  Shape('square');
} catch (error) {
  console.log(error.name);
}
▸ Expected output
function I am a circle 12.57
true
[ 'constructor', 'area' ]
false true
TypeError
Class syntaxBehind the scenes
class A { m() {} }function A() {} + A.prototype.m = function () {}
class B extends A {}Object.setPrototypeOf(B.prototype, A.prototype)
super.m()A.prototype.m.call(this)
static s() {}A.s = function () {}
The table is a simplified map: extends also links B to A so that static methods are inherited.
Exercise

Use Object.create(vehicle) to create two objects: car (brand: 'Toyota') and bike (brand: 'BMX' and its own property wheels: 2). The describe() method must come from the prototype in both. The lines below print the result.

Exercise · JavaScript
const vehicle = {
  wheels: 4,
  describe() {
    return `${this.brand} has ${this.wheels} wheels`;
  },
};

// create car and bike here

console.log(car.describe());
console.log(bike.describe());
console.log(Object.hasOwn(car, 'wheels'), Object.hasOwn(bike, 'wheels'));
▸ Expected output
Toyota has 4 wheels
BMX has 2 wheels
false true
Exercise

Add an increment() method to Counter.prototype: it increases value by 1 and returns this so calls can be chained. The method must be shared by all instances, not stored as an own property of each object.

Exercise · JavaScript
function Counter(start) {
  this.value = start;
}

// add increment() to Counter.prototype

const a = new Counter(5);
const b = new Counter(0);
a.increment().increment();

console.log(a.value, b.value);
console.log(a.increment === b.increment);
console.log(Object.hasOwn(a, 'increment'));
▸ Expected output
7 0
true
false

Key points

  • Every object has a hidden link to a prototype; a missing property is searched along the chain up to Object.prototype and then null.
  • Writing a property always creates an own property that shadows the inherited one; the prototype stays unchanged.
  • Object.create(proto) makes an object with the given prototype; new Fn() links the new object to Fn.prototype.
  • Methods on the prototype are shared by all instances; Object.hasOwn checks own properties, in checks the whole chain.
  • class is built on prototypes and adds extras: strict mode, a required new and private fields.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
After const a = { x: 1 }; const b = Object.create(a); b.x = 2;, what is a.x?