Home Blog CV Projects Patterns Notes Book Colophon Search

Prototypal Inheritance in ES6

6 Jan, 2017

Note: I'm going to come full circle and recommend not doing this after all via a series of updates later on ...

Here's an example using modern ES6 features for the classic prototypal inheritance example in JavaScript for node 6.9.2:

let alice = {
    name: "Alice",
    printName: function() {
        process.stdout.write(`${this.name}\n`);
    }
};
let bob = Object.create(alice);
bob.name = "Bob";

[alice, bob].forEach((person) => { 
    person.printName();
});

Prints:

Alice
Bob

What's nice about this is that no classes are needed to make the printName() method behave as you would expect, and you can override data in the from the prototype object just by assigning new keys to the derived object. In this case bob gets its own name key added with the value "Bob".

If you delete a key on the derived object, the old value will re-appear.

delete bob.name
bob.printName()

Prints:

Alice

This is because the bob object gets the value from alice since it is missing. You can prevent this by setting its value to undefined which is what JavaScript returns for a missing key anyway:

bob.name = undefined
bob.printName();

Prints:

undefined

Finally just because it is undefined doesn't mean the key isn't there:

for (item in bob) {
    if (bob.hasOwnProperty(item)) {
        process.stdout.write(`${item}\n`);
    }
}

Prints:

name

Notice that this doesn't include the properties inherited from alice, but you can get those by removing the if check:

for (item in bob) {
    process.stdout.write(`${item}\n`);
}

Prints:

name
printName

Let's put bob's name back to "Bob" and then add a new method to alice that also prints the object's name:

bob.name = 'Bob';
let anotherPrint = function() {
    process.stdout.write(`${this.name}\n`);
};
alice.anotherPrint = anotherPrint

If we use this new method on bob:

bob.anotherPrint()

We get:

Bob

What I really like about this is that this is correctly bound to the object the function is called from, even though the anotherPrint function is originally created as a plain old function.

Prototypal inheritance is a much underused feature of JavaScript IMHO.

Update 1, 14th Jan

One problem with this approach is that if you try to console.log() or JSON.stringify() an object created from another one, you'll only get the child object's properties.

This function creates a new object with everything flattened:

function flatten(input) {
    let output = Object.create(input);
    for(let key in input) {
        output[key] = input[key];
    }
    return output;
}

You can use it like this:

let alice = {
  name: "Alice",
  gender: "gender isn't important",
};
let bob = Object.create(alice);
bob.name = "Bob";
JSON.stringify(flatten(bob));

Gives:

'{"name":"Bob","gender":"gender isn\'t important"}'

Update 2, 14th Jan

I don't really like this, and having to jump through hoops to print out objects is annoying.

In 2014 Douglas Crockford updated his advice not to use Object.create() and instead to do this with ES6:

https://youtu.be/PSGEjv3Tqo0?t=1506

function constructor(spec) {
  let {member} = spec,
      {other} = other_constructor(spec),
      method = function () {
        // member, other, method, spec
      };
  return Object.freeze({
    method,
    other
  });
}

Here member is one thing extracted from spec, you could extract more, other is something extracted from another constructor, method is something we've created ourselves, and Object.freeze()returns a new object made up of all these parts, but one that can't be modified.

So, the same thing might be:

function person(spec) {
  let {name} = spec,
      printName = function() {
        process.stdout.write(`${name}\n`);
      }
  return Object.freeze({
    name,
    printName
  });
}

let alice = person({name: 'Alice'});
let bob = person({name: 'Bob'});

alice.printName();
bob.printName();

Gives:

Alice
Bob

In this example, bob isn't inherited from alice which makes sense. You can't say Bob is an Alice or even Bob has an Alice. But you could say Bob is a thing. In which case this might make sense:

function thing(spec) {
  let {name} = spec,
      printName = function() {
        process.stdout.write(`${name}\n`);
      }
  return Object.freeze({
    name,
    printName
  });
}

function person(spec) {
  let {name, printName} = thing(spec);
  return Object.freeze({
    name,
    printName
  });
}

let alice = person({name: 'Alice'});
let bob = person({name: 'Bob'});

alice.printName();
bob.printName();

This gives the same result.

Crockford points out that modern JavaScript engines like V8 can make stronger guarantees with this approach even though things are copied rather than inherited, because they know the exact shape the object will always take thanks to Object.freeze() so this might actually enable them to make better optimisations.

I also like that by explicitly placing what you want into the new object to be returned, you are not placing all the things you don't want.

Interestingly, Crockford also points out in the same talk that since 2014 he doesn't use null, new, class, this, for or while any more either. He can just do this to instead to replace the last two:

let arr = [1, 2, 3];
arr.forEach((value, i) => {
    console.log(value, i)
});

console.log()

let obj = {1: 'one', 2: 'two', 3: 'three'}
Object.keys(obj).forEach((key, i) => {
  let value = obj[key];
  console.log(key, value, i);
});

Gives:

1 0
2 1
3 2

1 one 0
2 two 1
3 three 2

Copyright James Gardner 1996-2020 All Rights Reserved. Admin.