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:
let person = {
getGreeting() {
return "Hello";
}
};
let dog = {
getGreeting() {
return "Woof";
}
};
// prototype is person
let friend = Object.create(person);
console.log(friend.getGreeting()); // "Hello"
console.log(Object.getPrototypeOf(friend) === person); // true
// set prototype to dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // "Woof"
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:
let person = {
getGreeting() {
return "Hello";
}
};
let dog = {
getGreeting() {
return "Woof";
}
};
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
}
};
// set prototype to person
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(Object.getPrototypeOf(friend) === person); // true
// set prototype to dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // "Woof, hi!"
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:
let friend = {
getGreeting() {
// in the previous example, this is the same as:
// Object.getPrototypeOf(this).getGreeting.call(this)
return super.getGreeting() + ", hi!";
}
};
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:
let friend = {
getGreeting: function() {
// syntax error
return super.getGreeting() + ", hi!";
}
};
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:
let person = {
getGreeting() {
return "Hello";
}
};
// prototype is person
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
}
};
Object.setPrototypeOf(friend, person);
// prototype is friend
let relative = Object.create(friend);
console.log(person.getGreeting()); // "Hello"
console.log(friend.getGreeting()); // "Hello, hi!"
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:
let person = {
getGreeting() {
return "Hello";
}
};
// prototype is person
let friend = {
getGreeting() {
return super.getGreeting() + ", hi!";
}
};
Object.setPrototypeOf(friend, person);
// prototype is friend
let relative = Object.create(friend);
console.log(person.getGreeting()); // "Hello"
console.log(friend.getGreeting()); // "Hello, hi!"
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.