Understanding JavaScript prototypes

Introduction

When you first learned JavaScript, you might have started by writing something simple like creating a string primitive:

const hello = 'Hello, world!';

You likely even learned how to use split to turn that string into an array of substrings:

const parts = hello.split(',');
console.log(parts); // output: ["Hello", " world!"]

You didn't implement split yourself, though. Instead, split is defined on hello's prototype object, which comes from String. Prototypes are JavaScript's method of inheritance and it allows properties to be shared across all object instances.

Prototypes

All JavaScript objects have a prototype, which is an object that it inherits properties from. This prototype object is a property on the constructor function that the inheriting object was created from, and the inheriting object links to it.

An object's prototype can have its own prototype, and that prototype can have its own prototype; this prototype chain continues until a prototype points to null, which is the end of the chain. Most objects are instances of Object, so the prototype chain will eventually link back to Object's prototype property, which is null.

This diagram, modified from MDN and created with Excalidraw, shows one way you can think about the prototypal inheritance of hello:

The prototype property and an object's prototype

A constructor function defines the prototype object on its prototype property; this is the object that all inheriting objects will link to. For example, to see all of the properties inherited by instances of String, we can log String.prototype:

console.log(String.prototype);

Output:

{
    anchor: ƒ anchor()
    big: ƒ big(),
    ...
    split: ƒ split()
    ...
    __proto__: Object
}

To access the prototype of an object, we can call Object.getPrototypeOf(obj) or use the __proto__ property of the object in many web browsers. Since hello is an instance of String (or, coerced to String at runtime), we should expect to see it linked to the prototype object defined by the String constructor function:

console.log(Object.getPrototypeOf(hello));

Output:

{
    anchor: ƒ anchor()
    big: ƒ big(),
    ...
    split: ƒ split()
    ...
    __proto__: Object
}

The prototype chain

We've discussed what prototypes are and how instances link to them, but how does this allow objects to inherit properties? To find the property of an object, JavaScript will "walk up" the prototype chain. First, it will look at the calling object's properties. If the property is not found there, it will look at its prototype's properties. This continues until the property is found or the end of the prototype chain is reached.

An instance of String is an object that inherits from Object, so String's prototype is the prototype defined on Object's constructor function. Because of this, we can access the properties defined on Object's prototype such as toLocaleString:

console.log(hello.toLocaleString()); // output: "Hello, world!"

When we called hello.toLocaleString(), JavaScript:

  1. Checked for the property on hello and did not find it
  2. Checked hello's prototype, the prototype object defined by String, and did not find it
  3. Checked String's prototype, the prototype object defined by Object, and did find it

Note: MDN is a handy way to tell which properties are defined on the prototype of built-in objects. For instance, the Array page links to documentation for all of the different properties, such as map and pop, that are defined on Array.prototype.

Walking the prototype chain in JavaScript

We briefly saw a simple graphical representation of hello's prototype chain earlier. Now that we know how to access an object's prototype, we can write our own function to show the chain programmatically:

function walkPrototypeChain(obj) {
    let current = Object.getPrototypeOf(obj);

    while (current) {
        console.log('Inherits from:', current.constructor.name);
        console.dir(current);

        const next = Object.getPrototypeOf(current);
        current = next;
    }

    console.log('Reached of prototype chain:', current);
}

Note: current.constructor.name is the name of the constructor function that defines the prototype.

If we run this in the browser with hello, we get the following output:

Extending a prototype

We can easily define our own properties on a constructor function's prototype property. Let's say we have a program that creates many arrays that we commonly want to ensure only contain truthy values. We can define a whereNotFalsy property on Array's prototype to make this available on every array we create:

Array.prototype.whereNotFalsy = function () {
    return this.filter((x) => x);
};

Now we can call whereNotFalsy on the subsequent arrays we create:

const hasFalsyValues = ['', 'Hello, world!', null];

console.log(hasFalsyValues.whereNotFalsy()); // output: ["Hello, world!"]

Conclusion

Prototypes allow objects to inherit shared properties. An object's prototype refers to the object that it inherits properties from. This prototype object is defined on the prototype property of the constructor function that creates it. Inheriting objects contain a link to the prototype object and it can be accessed through the __proto__ property in web browsers or by calling Object.getPrototypeOf in other contexts.

When an object's property is accessed, JavaScript first checks its own properties, then walks its prototype chain to find the property––this is how objects are able to inherit properties through prototypes. Lastly, we can directly modify the prototype of a constructor function by accessing its prototype property, which will affect all inheriting objects.

Let's connect

Come connect with me on LinkedIn, Twitter, and GitHub!

If you found this post helpful, please consider supporting my work financially:

☕️Buy me a coffee!

References


Subscribe for the latest news

Sign up for my mailing list to get the latest blog posts and content from me. Unsubscribe anytime.