import {NOTE} from './_util'
import daggy from 'daggy'

What is prototypal inheritance in JavaScript?

With the influx of developers from various backgrounds, the JavaScript scene has been 'enriched' with plenty of confusion around object orientation. The gist of it is that JavaScript OOP is not class-based, and grasping this idea is probably the key to clearing this confusion up and becoming a better JavaScript programmer.

The goal of this module is to demonstrate how prototypal inheritance works, and hopefully shorten the time it takes for novice JavaScript programmers to master it .

To get started, let's say we have two objects:

let printer = {
  print() {
    console.log(this.num)
  }
}

let counter = {
  increment() {
    this.num += 1
  },
  decrement() {
    this.num -= 1
  }
}

How do we combine these two objects so that we can use the functionality of both objects in a new object? The answer is prototypes. Let's create yet another object. This time the object will only have the num property to which the other two objects were referring to.

let thing = {
  num: 0
}

Let's set thing's prototype to printer so that we can use the print() function.

Object.setPrototypeOf(thing, printer)

Now we can call print() function as thing's property.

NOTE('thing.print()')
thing.print() // 0

Now let's modify the num property on thing and invoke print() again, to make sure that we're getting the correct result.

thing.num = 12
NOTE('thing.print() after setting num to 12')
thing.print() // 12

Now we know that the print() function has been borrowed from the printer object. We'll digress here a bit to note a few things about this borrowing.

The borrowing you see is done at runtime, not compile time. For instance, if we were to modify the original print() function, the modified version will be used even if we modified it after the prototype chain is established.

printer.print = function () {
  console.log('num is: ' + this.num)
}

NOTE('thing.print() after modifying printer.print()')
thing.print() // num is: 12

As you can see, this lookup is dynamic, and the updated print() function from printer is being used.

Now let's define a print function on thing itself, and see what happens.

thing.print = function () {
  console.log('I am thing')
}

NOTE('After adding a print function on thing')
thing.print() // I am thing

Defining the print() function on thing causes that version to be used instead. The lookup on printer is only done if thing does not have the property we are asking for. This new print property is called 'own property' to distinguish it from inherited properties.

For now, let's revert the last change by deleting the thing's own print property.

delete thing.print

NOTE('Calling thing.print() after deleting the own property')
thing.print() // num is: 12

We will now combine all three objects. To do this, we'll change the prototype chain a little.

Object.setPrototypeOf(counter, printer)
Object.setPrototypeOf(thing, counter)

As you can no doubt guess, we are setting the prototype chain such that thing's prototype is counter, whose prototype is printer. In other words, the chain now looks like thing -> counter -> printer.

We can still invoke the print() function as thing's property as before:

NOTE('thing.print() after changing the prototype chain to thing -> counter -> printer')
thing.print() // num is: 12

In addition, we also have access to the two functions in the counter object.

NOTE('thing.increment()')
thing.increment()
thing.print() // num is: 13

NOTE('thing.decrement()')
thing.decrement()
thing.print() // num is: 12

If we log thing itself, though, we'll learn something interesting.

NOTE('Log thing itself')
console.log(thing) // { num: 12 }

It does not list any of the properties from the prototypes. It only lists own properties. We can get a list of all properties -- own and inherited -- by looping over the keys:

NOTE('Looping over keys on thing')
for (let key in thing) {
  console.log('thing has key', key)

  // The `hasOwnProperty()` function can be used to test if some key is an own
  // property of an object. Incidentally, this method comes from the
  // `Object.prototype` which all objects have as the final prototype.

  if (thing.hasOwnProperty(key)) {
    console.log(key + ' is an own property')
  } else {
    console.log(key + ' is not an own property')
  }
}

To get all own properties as an array, we can use Object.keys() function.

NOTE('Object.keys(thing)')
console.log(Object.keys(thing)) // [ 'num' ]

Now thing is just one object. What if we want to create multiple versions of thing where each has a specific value of num? Kinda like... erm... classes? Armed with what we have seen thus far, it is not unimaginable that we could write functions for the purpose. It's not quite the same as a class in other languages, sure, but the effect is more or less the same.

const makeStuff = function (num = 0) {
  let thing = {num: num}
  Object.setPrototypeOf(thing, counter)
  return thing
}

let thing1 = makeStuff(5)
let thing2 = makeStuff(3)
let thing3 = makeStuff(100)

NOTE('thing1.print()')
thing1.print() // 5
NOTE('thing2.print()')
thing2.print() // 3
NOTE('thing3.print()')
thing3.print() // 100

The use of setPrototypeOf() function to create prototype chains is used very rarely in real life, if at all. The main reason for this is that it changes the prototype of an already instantiated object, which can be slow when the object you've created is already referenced in many places in your code when you call setPrototypeOf() on it. You should definitely not use it in such a scenario (fixing inheritance after the fact).

For object creation, though, the use of setPrototypeOf() seems to be on par with other methods of creating objects in terms of performance. Therefore, there is no reason to avoid it.

Another way to establish prototypal inheritance is to use Object.create(). This method is discussed in much more depth by Douglas Crockford in his famous article entitled Prototypal inheritance (see http://javascript.crockford.com/prototypal.html). Since he wrote the article, the example code has made it into the official ECMAScript 5 specification and is now supported in virtually all JavaScript engines.

Let's replicate the above three objects using Object.create(). We'll leave printer alone, as it is the last member of the chain and we don't need to do anything special.

counter = Object.create(printer)

This code creates an empty counter object which has its prototype set to printer. Since it's an empty object, we need to add the two own properties. In ECMAScript 2015 (a.k.a. ECMAScript 6), we can use Object.assign() to create all own properties using an object. This approach is not possible in earlier versions of JavaScript, where you have to add properties one by one. We'll see both approaches, starting with the old approach first.

counter.increment = function () {
  this.num += 1
}

counter.decrement = function () {
  this.num -= 1
}

The new way to achieve the above (and the above is still a valid approach) would be:

Object.assign(counter, {
  increment() {
    this.num += 1
  },
  decrement() {
    this.num -= 1
  }
})

Although I say 'new way', it does not do exactly the same thing as assigning properties. The way we use Object.assign() here, a new object literal is first created, and then its own properties are copied to the counter object after that. This is probably fast enough in most situations, but it stands to reason that it would be slower than assigning properties one by one. In most cases, I imagine it will not be an issue. Profiling is your friend, though.

Now counter is equipped with the same own properties as the original counter. Finally we create thing which inherits the properties on counter. For this we will write a new version of the makething function using Object.create() and Object.assign().

const createThing = function (num = 0) {
  let thing = Object.create(counter)
  Object.assign(thing, {
    num: num
  })
  return thing
}

thing = createThing(0)

We can now use the prototype chain to do what we could do with the original three objects.

NOTE('thing.increment() twice')
thing.increment() // thing.num === 1
thing.increment() // thing.num === 2

NOTE('thing.decrement() once')
thing.decrement() // thing.num === 1

NOTE('thing.print()')
thing.print() // num is: 1

We will not go into too much detail on constructor functions and classes, since there is plenty of material on those already. It should be noted, though, that prototypal inheritance is at the heart of both ways of object creation in JavaScript and you should not forget that. In this guide, I will just show some examples of how to replicate the prototype chain using the two methods mentioned. For brevity, I will leave printer and counter alone, and demonstrate just the thing part.

function Thing(num = 0) {
  this.num = num
}
Thing.prototype = counter

thing = new Thing(0)

NOTE('thing.increment()')
thing.increment() // thing.num === 1

NOTE('thing.print()')
thing.print() // num is: 1

A few notes about the constructor function. The constructor function is a normal function just like any other. There is no hidden magic in it. It capitalized as a convention as a cue that it's a constructor. The trick is in the new keyword. When a function is invoked with the new keyword, a new blank object is created. The object's prototype is set to the constructor's prototype property (in our case, it's Thing.prototype which we point to counter), and the function is invoked with this set to the newly created object. Anything we do to this in the constructor is done to the new object (in our example, assigning the num property).

NOTE: see the not-new module for an implementation of the new keyword as a JavaScript function.

A common complaint about constructor functions was the requirement to use new. This is actually quite easy to mitigate with a little bit of code.

function AnotherThing(name) {
  if (!this) return new AnotherThing(name)
  // Do the normal constructor things below:
  this.name = name
}

AnotherThing.prototype.print = function () {
  console.log('another thing:', this.name)
}

const another1 = AnotherThing('without new')
const another2 = new AnotherThing('with new')

another1.print() // another thing: without new
another2.print() // another thing: with new

The reason it works without new is that when invoked without new, the this is undefined (you need to use strict mode for this, though, so be careful).

As mentioned before, ECMAScript 6 introduced a new class keyword to help out developers that feel more comfortable thinking in terms of classes. The keyword does not really change the fact that we are doing prototypal inheritance, though, and if you fancy using it, you should be aware of this fact.

Again, doing thing as the example, we will implement an ES6 class. This time, though, we cannot do direct inheritance from the counter object as ES6 classes can only inherit other classes or constructors.

For the purposes of this example, let's just imagine we have a counter class that implements counter.

class Counter /* extends printer */ {
  constructor() { /* ... */ }
}

// We use 'Thing1' below because we cannot have multiple classes and
// constructors  of the same name

class Thing1 extends Counter {
  constructor(num = 0) {
    super() // <-- must call this when using inheritance
    this.num = num
  }
}

You will notice the super() call in the Thing1's constructor. This is a shortcut for invoking the Counter's constructor and it is required if you need to manipulate this in the constructor. This is good example of why I think the classes in ES6 introduce completely unnecessary levels of complexity, and why I personally don't find them so useful.

If we don't want to have a counter class, and we want to stick to using counter object we can still do that.

class Thing2 {
  constructor(num = 0) {
    this.num = num
  }
}

Thing2.prototype = counter

In this case, we don't need to invoke super() as there is no superclass. We can also clearly see that classes also have the prototype property which works the same way as the prototype property on constructors.

Instantiating the class is exactly the same as with constructors (the whole class business is actually just another way to write constructors).

NOTE('Create thing using a class')
thing = new Thing2(8)
thing.increment() // thing.num === 9
thing.print() // 9

All of the built-in types in JavaScript are implemented as or have matching constructor functions. For instance, Date, Array, String and similar, are all constructors. This is the reason you will frequently see references to Array.prototype.slice() and similar functions, which are properties on the objects created by the mentioned constructors. Array.prototype.slice() is the same as [].slice(), and the former notation is simply used as a convention (possibly because [].slice() looks a bit weird in documentation).

TRIVIA: Math is named like a constructor, but it is not.

If you want to create a constructor function quickly, and you don't care about inheritance at all (there are valid use cases for this in declarative programming), there is a very nice library called daggy. Although I don't normally recommend any particular library for anything, daggy is an exception because it is very small, and it doesn't try to do too many things at once.

To create a constructor using daggy, you just call the tagged() function. For example:

const Thing3 = daggy.tagged('name')

Thing.prototype.print = function () {
  console.log(this.name)
}

Then you can work with it as with a normal constructor. The library doesn't just create constructors. The arguments passed to tagged() become the constructor arguments an the object properties. The constructor also checks if all the arguments were supplied, and allows the usage without new. In a way, daggy's tagged constructors are constructors on steroids.

try {
  let thing3 = Thing3()
} catch (e) {
  console.log('Could not create Thing3 without arguments')
}

With this, we conclude the module on prototypal inheritance. Remember that there is no one correct way to do these things in JavaScript, and with ES5 and ES6 we now have more options than ever. On the other hand, prototypal model is at the heart of it all in JavaScript, and that is the single truth that you have to keep in mind whatever path you choose.

results matching ""

    No results matching ""