More Powerful Prototypes

Prototypes are the foundation of inheritance in JavaScript, and ECMAScript 6 continues to make prototypes more powerful. Early versions of JavaScript severely limited what could be done with prototypes. However, as the language matured and developers became more familiar with how prototypes work, it became clear that developers wanted more control over prototypes and easier ways to work with them. As a result, ECMAScript 6 introduced some improvements to prototypes.

Changing an Object’s Prototype

Normally, the prototype of an object is specified when the object is created, via either a constructor or the Object.create() method. The idea that an object’s prototype remains unchanged after instantiation was one of the biggest assumptions in JavaScript programming through ECMAScript 5. ECMAScript 5 did add the Object.getPrototypeOf() method for retrieving the prototype of any given object, but it still lacked a standard way to change an object’s prototype after instantiation.

ECMAScript 6 changes that assumption by adding the Object.setPrototypeOf() method, which allows you to change the prototype of any given object. The Object.setPrototypeOf() method accepts two arguments: the object whose prototype should change and the object that should become the first argument’s prototype. For example:

  1. let person = {
  2. getGreeting() {
  3. return "Hello";
  4. }
  5. };
  6. let dog = {
  7. getGreeting() {
  8. return "Woof";
  9. }
  10. };
  11. // prototype is person
  12. let friend = Object.create(person);
  13. console.log(friend.getGreeting()); // "Hello"
  14. console.log(Object.getPrototypeOf(friend) === person); // true
  15. // set prototype to dog
  16. Object.setPrototypeOf(friend, dog);
  17. console.log(friend.getGreeting()); // "Woof"
  18. console.log(Object.getPrototypeOf(friend) === dog); // true

This code defines two base objects: person and dog. Both objects have a getGreeting() method that returns a string. The object friend first inherits from the person object, meaning that getGreeting() outputs "Hello". When the prototype becomes the dog object, friend.getGreeting() outputs "Woof" because the original relationship to person is broken.

The actual value of an object’s prototype is stored in an internal-only property called [[Prototype]]. The Object.getPrototypeOf() method returns the value stored in [[Prototype]] and Object.setPrototypeOf() changes the value stored in [[Prototype]]. However, these aren’t the only ways to work with the value of [[Prototype]].

Easy Prototype Access with Super References

As previously mentioned, prototypes are very important for JavaScript and a lot of work went into making them easier to use in ECMAScript 6. Another improvement is the introduction of super references, which make accessing functionality on an object’s prototype easier. For example, to override a method on an object instance such that it also calls the prototype method of the same name, you’d do the following:

  1. let person = {
  2. getGreeting() {
  3. return "Hello";
  4. }
  5. };
  6. let dog = {
  7. getGreeting() {
  8. return "Woof";
  9. }
  10. };
  11. let friend = {
  12. getGreeting() {
  13. return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
  14. }
  15. };
  16. // set prototype to person
  17. Object.setPrototypeOf(friend, person);
  18. console.log(friend.getGreeting()); // "Hello, hi!"
  19. console.log(Object.getPrototypeOf(friend) === person); // true
  20. // set prototype to dog
  21. Object.setPrototypeOf(friend, dog);
  22. console.log(friend.getGreeting()); // "Woof, hi!"
  23. console.log(Object.getPrototypeOf(friend) === dog); // true

In this example, getGreeting() on friend calls the prototype method of the same name. The Object.getPrototypeOf() method ensures the correct prototype is called, and then an additional string is appended to the output. The additional .call(this) ensures that the this value inside the prototype method is set correctly.

Remembering to use Object.getPrototypeOf() and .call(this) to call a method on the prototype is a bit involved, so ECMAScript 6 introduced super. At its simplest, super is a pointer to the current object’s prototype, effectively the Object.getPrototypeOf(this) value. Knowing that, you can simplify the getGreeting() method as follows:

  1. let friend = {
  2. getGreeting() {
  3. // in the previous example, this is the same as:
  4. // Object.getPrototypeOf(this).getGreeting.call(this)
  5. return super.getGreeting() + ", hi!";
  6. }
  7. };

The call to super.getGreeting() is the same as Object.getPrototypeOf(this).getGreeting.call(this) in this context. Similarly, you can call any method on an object’s prototype by using a super reference, so long as it’s inside a concise method. Attempting to use super outside of concise methods results in a syntax error, as in this example:

  1. let friend = {
  2. getGreeting: function() {
  3. // syntax error
  4. return super.getGreeting() + ", hi!";
  5. }
  6. };

This example uses a named property with a function, and the call to super.getGreeting() results in a syntax error because super is invalid in this context.

The super reference is really powerful when you have multiple levels of inheritance, because in that case, Object.getPrototypeOf() no longer works in all circumstances. For example:

  1. let person = {
  2. getGreeting() {
  3. return "Hello";
  4. }
  5. };
  6. // prototype is person
  7. let friend = {
  8. getGreeting() {
  9. return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
  10. }
  11. };
  12. Object.setPrototypeOf(friend, person);
  13. // prototype is friend
  14. let relative = Object.create(friend);
  15. console.log(person.getGreeting()); // "Hello"
  16. console.log(friend.getGreeting()); // "Hello, hi!"
  17. console.log(relative.getGreeting()); // error!

The call to Object.getPrototypeOf() results in an error when relative.getGreeting() is called. That’s because this is relative, and the prototype of relative is the friend object. When friend.getGreeting().call() is called with relative as this, the process starts over again and continues to call recursively until a stack overflow error occurs.

That problem is difficult to solve in ECMAScript 5, but with ECMAScript 6 and super, it’s easy:

  1. let person = {
  2. getGreeting() {
  3. return "Hello";
  4. }
  5. };
  6. // prototype is person
  7. let friend = {
  8. getGreeting() {
  9. return super.getGreeting() + ", hi!";
  10. }
  11. };
  12. Object.setPrototypeOf(friend, person);
  13. // prototype is friend
  14. let relative = Object.create(friend);
  15. console.log(person.getGreeting()); // "Hello"
  16. console.log(friend.getGreeting()); // "Hello, hi!"
  17. console.log(relative.getGreeting()); // "Hello, hi!"

Because super references are not dynamic, they always refer to the correct object. In this case, super.getGreeting() always refers to person.getGreeting(), regardless of how many other objects inherit the method.