Prototypal Inheritance in JavaScript

As JavaScript developers we use prototypal inheritance in our code every day. I decided I needed to understand this key feaure of JavaScript better. This is what I learned.

Contents

    What is Prototypal Inheritance?

    According to the MDN docs: "When it comes to inheritance, JavaScript only has one construct: objects. Each object has a private property which holds a link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. By definition, null has no prototype, and acts as the final link in this prototype chain."



    Prototypes are the mechanisms which power inheritance in JavaScript (JS). Prototypes are simply objects which contain a predefined set of values, which we can access and make use of. Each type has it's own predefined prototypal object.

    const myObj = {}
    
    console.log(myObj)
    // {}
    //   __proto__:
    //   constructor: ƒ Object()
    //   __defineGetter__: ƒ __defineGetter__()
    //   __defineSetter__: ƒ __defineSetter__()
    //   hasOwnProperty: ƒ hasOwnProperty()
    //   __lookupGetter__: ƒ __lookupGetter__()
    //   __lookupSetter__: ƒ __lookupSetter__()
    //   isPrototypeOf: ƒ isPrototypeOf()
    //   propertyIsEnumerable: ƒ propertyIsEnumerable()
    //   toString: ƒ toString()
    //   valueOf: ƒ valueOf()
    //   toLocaleString: ƒ toLocaleString()
    //   get __proto__: ƒ __proto__()
    //   set __proto__: ƒ __proto__()

    In the example above, we can see the __proto__ property (sometimes called the 'dunder proto') on myObj. Whenever an object is instantiated in JavaScript, provided it isn't mutated, JS automatically links it to the global object prototype through the __proto__ property.



    What does this mean, and why is it relevant?



    Well, in the example above you'll notice we didn't explicitly define any properties inside of myObj - in other words the value of myObj is equal to an empty object literal. So if that's the case, how come we can do the following:

    const myObj = {}
    
    console.log(myObj.toString())
    // "[object Object]"

    We're able to access a property toString() on myObj, yet we haven't declared a property called toString inside myObj. myObj is just an empty object literal. Notice also the log statement doesn't return undefined.


    When calling a property on an object (or other primitive type), JS will first look inside the definition of the object for the property that is called. If the property does not exist inside the object definition, then JS looks inside the __proto__ object for the property. In a nutshell, this is what prototypal inheritance is in JS.


    Now, if we explicitly declare a toString property on myObj, then it will get priority over the toString method set inside __proto__:

    const myObj = {}
    
    myObj.toString = () => {
      console.log('New property `toString` invoked!!')
    }
    
    console.log(myObj.toString())
    // "New property `toString` invoked!!"




    Primitive Types and Sub-Types

    All types of objects within JS have their own built-in global prototype objects which are linked as soon as the object is instantiated. This is also true for other primitive types (more on this later), and also object sub-types, such as arrays, maps, sets and functions.

    const myArr = []
    
    console.log(myArr)
    // []
    //   length: 0
    //   __proto__: Array(0)
    //   length: 0
    //   constructor: ƒ Array()
    //   concat: ƒ concat()
    //   copyWithin: ƒ copyWithin()
    //   fill: ƒ fill()
    //   find: ƒ find()
    //   findIndex: ƒ findIndex()
    //   lastIndexOf: ƒ lastIndexOf()
    //   pop: ƒ pop()
    //   push: ƒ push()
    //   reverse: ƒ reverse()
    //   shift: ƒ shift()
    //   unshift: ƒ unshift()
    //   slice: ƒ slice()
    //   sort: ƒ sort()
    //   splice: ƒ splice()
    //   includes: ƒ includes()
    //   indexOf: ƒ indexOf()
    //   join: ƒ join()
    //   keys: ƒ keys()
    //   entries: ƒ entries()
    //   values: ƒ values()
    //   forEach: ƒ forEach()
    //   filter: ƒ filter()
    //   flat: ƒ flat()
    //   flatMap: ƒ flatMap()
    //   map: ƒ map()
    //   every: ƒ every()
    //   some: ƒ some()
    //   reduce: ƒ reduce()
    //   reduceRight: ƒ reduceRight()
    //   toLocaleString: ƒ toLocaleString()
    //   toString: ƒ toString()
    //   Symbol(Symbol.iterator): ƒ values()
    //   Symbol(Symbol.unscopables): {copyWithin: true, entries: true, fill: true, find: true, findIndex: true, …}
    //   __proto__: Object




    The Difference Between .prototype and __proto__

    Functions in JS are first-class objects. This means they can have functions and methods associated with them as properties just like objects can.


    Whenever we instantiate a new function (not an arrow function), JS automatically creates a property on that function called .prototype.

    function myFunc() {}
    
    console.log(myFunc.prototype)
    // {constructor: ƒ}
    //   constructor: ƒ myFunc()
    //   __proto__: Object

    .prototype has two properties: constructor and __proto__. When a property is called on a function using the dot notation, JS does not look in the .prototype property, but it does look in the __proto__ property. In other words, the .prototype property is not used for prototypal inheritance, whereas the __proto__ property is, even though __proto__ is not a direct property of the function.

    function myFunc() {}
    
    myFunc.prototype.newString = 'Hello I am a new string'
    
    console.log(myFunc.prototype)
    // { newString: "Hello I am a new string", constructor: ƒ }
    //   newString: "Hello I am a new string"
    //   constructor: ƒ myFunc()
    //   __proto__: Object
    
    const myNewFunc = new myFunc()
    
    console.log(myNewFunc)
    // myFunc {}
    //   __proto__:
    //     newString: "Hello I am a new string"
    //     constructor: ƒ myFunc()
    //     __proto__: Object
    
    console.log(myNewFunc.prototype)
    // undefined

    Here we've added a new property newString to myFunc. Then we instantiated a new constructor of myFunc called myNewFunc. When adding a property to a function, it is added to the .prototype object. But, if we declare a new instance of that function using the new keyword, the new instance will have that new property available to it in the __proto__ object, rather than the .prototype object.


    Why should you care?


    console.log(Object)
    // ƒ Object() { [native code] }
    
    console.log(Array)
    // ƒ Array() { [native code] }
    
    console.log(Map)
    //ƒ Map() { [native code] }
    
    console.log(Set)
    // ƒ Set() { [native code] }

    These are all global functions, not objects. They all have access to a .prototype property, which has methods inside of it. Many JS developers use these on a regular basis without even thinking about it. For example, every time you map or forEach through an array, you're actually using the global array.prototype.map or global array.prototype.forEach method. When the array is created, JS automatically creates a connection between our new array, and the corresponding __proto__ property.

    console.log(Object.prototype)
    // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
    
    console.log(Array.prototype)
    // [constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]
    
    console.log(Map.prototype)
    // Map {constructor: ƒ, get: ƒ, set: ƒ, has: ƒ, delete: ƒ, …}
    
    console.log(Set.prototype)
    // Set {constructor: ƒ, has: ƒ, add: ƒ, delete: ƒ, clear: ƒ, …}

    In other words, you're using prototypal inhheritance any time you use any type of object in your code.




    Primitive Types and the Prototype Chain

    It's not just objects that have access to the prototype chain:

    console.log(Boolean.prototype)
    // Boolean {false, constructor: ƒ, toString: ƒ, valueOf: ƒ}
    
    console.log(Number.prototype)
    // Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, …}
    
    console.log(String.prototype)
    // String {"", constructor: ƒ, anchor: ƒ, big: ƒ, blink: ƒ, …}
    
    console.log(Symbol.prototype)
    // Symbol {Symbol(Symbol.toStringTag): "Symbol", constructor: ƒ, toString: ƒ, valueOf: ƒ, …}
    
    console.log(BigInt.prototype)
    // BigInt {Symbol(Symbol.toStringTag): "BigInt", constructor: ƒ, toLocaleString: ƒ, toString: ƒ, valueOf: ƒ}

    While these primitive types are not technically objects, JS gives them behaviour which allows them to behave like objects, and so they have access to the prototype chain.




    Summary

    Objects in JS give us the ability to create a group of key/value pairs. We can look up properties in objects using the dot notation. As expected, if we tried to look up a property which hasn't been declared on an object, we'd get undefined:

    const myObj = {
      firstName: 'Bobbery'
    }
    
    console.log(myObj.firstName)
    // Bobbery
    
    console.log(myObj.secondName)
    // undefined

    That's true unless the property is part of the object's prototype chain:

    const myObj = {
      firstName: 'Bobbery'
    }
    
    console.log(myObj.firstName)
    // Bobbery
    
    console.log(myObj.toString())
    // "[object] [Object]"

    In which case we can make use of JavaScript's prototypal inheritance system.




    Thanks for reading!

    That's it. That's all I've got for today. I hope you found it useful. Feel free to get in touch if I've made any mistakes in this post, if you fancy a chat about prototypal inheritance, or for anything else, by using one of the links below.