How JavaScript Prototypes Work: A Simple Explanation

How JavaScript Prototypes Work: A Simple Explanation

To make you comfortable with the word Prototype I recommend you to think of it as a Family Tree where you inherit some properties from your parents like hair color, height, blood type, skin tone, etc. to yourself. In the same way JavaScript objects inherits properties and methods from there prototype (or parent):

  • Date objects inherit from Date.prototype

  • Array objects inherit from Array.prototype

  • Object inherit from Object.prototype

NOTE

Note

The Chrome console uses [[Prototype]] to denote the object's prototype(or parent), following 
the ECMAScript terms; Firefox uses <prototype> to denote the object's prototype(or parent). 
For consistency I will use [[Prototype]].

Before going forward I want to tell you some important terminologies that I will use in this blog, detailed explanation is given in later section:

  • [[Prototype]]/<prototype>

    This notation is used in browsers to represent who is the parent (or prototype) of the object. It does not set the parent (or prototype) of the object or get the parent (or prototype) of the object it simply tells who is the parent (or prototype) of object.

  • __proto__

    It is a property in JavaScript that has the power to set the parent (or prototype) of the object or get the parent (or prototype) of the object.

  • .prototype

    It is a special property that is only available on a constructor function and it becomes the parent (or prototype) of every object that is created from that constructor function.

Prototype and Prototype Chain

Let understand what is Prototype and Prototype Chain in JavaScript.

In JavaScript, every object has a special hidden property [[Prototype]] (that tell who is parent of this object) that is either null or references another object.

When we try to access any property of an object and that property is not available on the object, than JavaScript does not stop here it then start searching the parent (or prototype) of the object, the parent (or prototype) of the parent (or prototype), and so on until either a property with a matching name is found or the end of the prototype chain is reached.


let obj = {
 name: "object"
}

console.log(obj.name)  // logs: object

// But if we try to access an property that is not
// available on the object than it's prototype is 
// searched which in this case is Object.prototype

console.log(obj.toString())  // logs: [object Object]

// In above console.log() we try to access toString() function which is not 
// available in "obj" but as we know JS does not stop here and it start searching
// the prototype of "obj" which is "Object.prototype" and it has a function defined
// by name toString() so JS takes this function from prototype and execute it here.

// The Prototype (or parent) chain for above example will look like this
{name: "object"} ---> Object.prototype ---> null

Prototype (or parent) chain is nothing but it simply is that JS start searching the parent (or prototype) of the object when it does not find any property or method on the object and it find it until the property of matching name is found or null is reached which is end of prototype chain so this searching of parent (or prototype) of object in JS creates a chain of object to which we refer as prototype chain.

For example in above code when JS does not find toString() method on obj it start searching the parent (or prototype) of obj which is Object.prototype and found a method name toString() if JS does not found a method name toString() on Object.prototype than it will search for the parent (or prototype) of Object.prototype which is in this case null and it will return undefined as null does not have method name toString(). This searching of method or property on parent (or prototype) of object creates a chain of object which is refer to as Prototype Chain.

Prototype Chain of above example.

Another example for this is:

const o = {
  a: 1,
  b: 2,
  // __proto__ sets the [[Prototype]]. It's specified here
  // as another object literal. (This is explained in later section.)
  __proto__: {
    b: 3,
    c: 4,
  },
};

// o.[[Prototype]] has properties b and c because {b: 3, c: 4} is parent (or prototype) of 
// "o" object.
// o.[[Prototype]].[[Prototype]] is Object.prototype because every object has "Object.prototype"
// as it parent if we don't change the prototype chain.
// Finally, o.[[Prototype]].[[Prototype]].[[Prototype]] is null.
// This is the end of the prototype chain, as null,
// by definition, has no [[Prototype]].
// Thus, the full prototype chain looks like:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null

console.log(o.a); // 1
// Is there an 'a' own property on o? Yes, and its value is 1.

console.log(o.b); // 2
// Is there a 'b' own property on o? Yes, and its value is 2.
// The prototype also has a 'b' property, but it's not visited.
// This is called Property Shadowing

console.log(o.c); // 4
// Is there a 'c' own property on o? No, check its prototype.
// Is there a 'c' own property on o.[[Prototype]]? Yes, its value is 4.

console.log(o.d); // undefined
// Is there a 'd' own property on o? No, check its prototype.
// Is there a 'd' own property on o.[[Prototype]]? (which is this object { b: 3, c: 4 } )
// No, check its prototype.
// o.[[Prototype]].[[Prototype]] is Object.prototype and
// there is no 'd' property by default, check its prototype.
// o.[[Prototype]].[[Prototype]].[[Prototype]] is null, stop searching,
// no property found, return undefined.

we will learn more about __proto__ in later section and in above example we are using o.[[Prototype]] this notation to get object parent (or prototype) but in real use cases we use Object.getPrototypeOf() to get the parent (or prototype) of an object and Object.setPrototypeOf() to set the parent (or prototype) of an object. we will learn more about them in later section.

Similarly, we can create longer prototype chains, and a property will be sought on all of them.

const o = {
  a: 1,
  b: 2,
  // __proto__ sets the [[Prototype]]. It's specified here
  // as another object literal.
  __proto__: {
    b: 3,
    c: 4,
    __proto__: {
      d: 5,
    },
  },
};

// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null

console.log(o.d); // 5

Some Important terminologies of Prototypes

  • __proto__

  • .prototype

  • Object.getPrototypeOf()

  • Object.setPrototypeOf()

__proto__

The __proto__ accessor property of an Object instances is used to set the parent (or prototype) of the object or get the parent (or prototype) of the object.

For Example

  • Here, we are using __proto__ accessor property to get the prototype(or parent) of the object which return is simply the reference to [[Prototype]] hidden property of an object.

    • For objects created using an object literal this value is Object.prototype.

    • For objects created using array literals, this value is Array.prototype.

    • For functions, this value is Function.prototype.

    // OBJECTS
    const obj = {
        a: 1,
        b: 2,
    }

    console.log(obj.__proto__)

    // obj prototype
    {__defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ,
     __lookupSetter__: ƒ, …}

    // ARRAYS
    const arr = [1, 2, 3, 4]

    console.log(arr.__proto__)

    // arr prototype
    [at: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]

    // Functions
    function hello() {
        return "Hello"
    }

    console.log(hello.__proto__)

    // hello function prototype
    {apply: ƒ, bind: ƒ, call: ƒ, arguments: (...), …} (only accessible in firefox console)
  • The __proto__ also allow to change the parent (or prototype) of an object like:

       const obj = {
          a: 1,
          b: 2,
      }
    
      const objTwo = {
          c: 3,
          d: 4,
      }
    
      const objThree = {
          e: 5,
          f: 6,
      }
    
      obj.__proto__ = objTwo 
      // doing this change the prototype of obj from Object.prototype
      //  (shown in above example) to objTwo ({ c: 3, d: 4}).
    
      objTwo.__proto__ = objThree
    
      console.log(obj.__proto__)
      // "obj" Object Prototype
      // {c: 3, d: 4}
    
      console.log(objTwo.__proto__)  // {e: 5, d: 6}
    
      console.log(objThree.__proto__) // {__defineGetter__: ƒ, __defineSetter__: ƒ, …}
    

    But there is one thing to keep in mind here is that the value passed to obj.__proto__ must be an object or null. Providing any other value to obj.__proto__ will do nothing.

       const obj = {
          a: 1,
          b: 2,
      }
    
      obj.__proto__ = "hello"
      // OR 
      // obj.__proto__ = 66   // It will do nothing.
    
      console.log(obj.__proto__)
    
      // obj prototype is still this
      {__defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ,
       __lookupSetter__: ƒ, …}
    

But setting or reading the prototype with obj.__proto__ is considered outdated and somewhat deprecated (moved to the so-called “Annex B” of the JavaScript standard, meant for browsers only).

The modern methods to get/set a prototype are:

  • Object.getPrototypeOf(obj) – returns the [[Prototype]] of obj.

  • Object.setPrototypeOf(obj, proto) – sets the [[Prototype]] of obj to proto.

The only usage of __proto__, that’s not frowned upon, is as a property when creating a new object: { __proto__: ... }.

const o = {
  a: 1,
  b: 2,
  // __proto__ sets the [[Prototype]]. It's specified here
  // as another object literal.
  __proto__: {
    b: 3,
    c: 4,
  },
};

.prototype

In JavaScript, .prototype is a special property that exists only on the functions that can be used as a constructor function. It is not accessible on the instances of objects.

Who can access .prototype property

Every function that is meant to be used as a constructor function can access this property.

  1. Constructor Functions

     function Person() {}
    
     console.log(Person.prototype). // { constructor : ƒ Person(),[[Prototype]]: Object }
    
  2. Classes

     class Student {}
    
     console.log(Student.prototype) // { constructor : class Student,[[Prototype]]: Object }
    
  3. Built-in constructors

Who cannot access .prototype property

These are functions, objects and methods that cannot be used as a constructor function so they cannot access the .prototype property.

  1. Arrow Function

     const arrowFun = () => {}
     console.log(arrowFun.prototype) // undefined
    
  2. Object and Class instances

     const obj = {}
    
     class Car {}
    
     const myCar = new Car() 
    
     console.log(myCar.prototype) // undefined
     console.log(obj.prototype) // undefined
    
  3. Methods inside objects or classes

     const person = {
         speak() {}
     };
     console.log(person.speak.prototype); //  undefined
    
  4. async Functions

     async function AsyncFunction() {}
    
     console.log(AsyncFunction.prototype) // undefined
    

What is .prototype property ?

The .prototype property of a constructor function is used to set the parent (or prototype) of the every new object that is being created from the constructor function.

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

Student.prototype.name = function() {
    return `My name is ${this.name}.`
}

Student.prototype.age = function() {
    return `My age is ${this.age}.`
}

const studentOne = new Student("John", 33)
const studentTwo = new Student("Jane", 23)
const studentThree = new Student("Jo", 30)

console.log(studentOne)    // Object { name: "John", age: 33 }
console.log(studentTwo)    // Object { name: "Jane", age: 23 }
console.log(studentThree)  // Object { name: "Jo", age: 30 }

// Since "obj" is created from the Student Constructor function so it's parent (or prototype)
// is the constructor function .prototype property

console.log(Object.getPrototypeOf(studentOne)) // Object { name: name(), age: age(), constructor: function Student(name, age), ... }
console.log(Object.getPrototypeOf(studentTwo)) // Object { name: name(), age: age(), constructor: function Student(name, age), ... }
console.log(Object.getPrototypeOf(studentThree)) // Object { name: name(), age: age(), constructor: function Student(name, age), ... }

console.log(Object.getPrototypeOf(studentOne) === Student.prototype)       // true
console.log(Object.getPrototypeOf(studentTwo) === Student.prototype)       // true
console.log(Object.getPrototypeOf(studentThree) === Student.prototype)     // true

If that a too much to absorb let me sum up for you:

  • [[Prototype]]/<prototype>

    This notation is used in browsers to represent who is the parent of the object. It does not set or get the prototype it simply tells who is the prototype of object.

  • __proto__

    It is a property in JavaScript that has the power to get or set the [[Prototype]](or parent) of the object.

  • .prototype

    It is a special property that is only available on a constructor function and it becomes the prototype(or parent) of every object that is created from a constructor function.

Conclusion

JavaScript's prototype chain is a powerful mechanism that enables objects to inherit properties and methods efficiently. Understanding how the prototype chain works helps in debugging, improving performance, and writing cleaner, more maintainable JavaScript.