In last week’s post, I wrote that you can use the super
keyword to invoke a method on an object’s prototype. Though much older, the Function.prototype.call
method is a useful—and more versatile—alternative.
Here’s the super
example from last week. In the body of the warriorProto.greet
method, we use the super
keyword to invoke the heroProto.greet
method:
let heroProto = {
greet() {
return `As a hero, ${this.name} says hello.`;
},
};
let warriorProto = {
__proto__: heroProto,
greet() {
let greeting = super.greet(); // Invoke method on prototype
greeting += `\nAs a warrior, ${this.name} says "take no prisoners!"`;
return greeting;
},
};
let warrior = {
__proto__: warriorProto,
name: "Bjorn",
level: 1,
weapon: "axe",
};
Modified to use the Function.prototype.call
method, the body of the warriorProto.greet
method looks like this:
let warriorProto = {
__proto__: heroProto,
greet() {
let greeting = heroProto.greet.call(this); // Invoke method on prototype
greeting += `\nAs a warrior, ${this.name} says "take no prisoners!"`;
return greeting;
},
};
Instead of invoking super.greet()
, we invoke heroProto.greet.call(this)
. Because heroProto.greet
is a Function
object, it has access to the Function.prototype.call
method. This lets us invoke heroProto.greet
with a given this
value.
As long as we have an object with a .name
property, we can pass it to heroProto.greet.call()
and it will work—even if the object doesn’t inherit from heroProto
. This arguably makes the Function.prototype.call
method more versatile than the super
keyword:
let heroProto = {
greet() {
return `As a hero, ${this.name} says hello.`;
},
};
let dragonborn = {
name: "Dovahkiin",
};
let greeting = heroProto.greet.call(dragonborn);
console.log(greeting); // "As a hero, Dovahkiin says hello."
Before spread syntax and the Array.from()
method, this technique was often used to convert an array-like object to a true array. It works because, like a true array, an array-like object has numbered keys and a .length
property, which is what the Array.prototype.slice()
method expects:
let divs = document.getElementsByTagName("div");
let isArray = Array.isArray(divs);
console.log(isArray); // false
divs = Array.prototype.slice.call(divs);
isArray = Array.isArray(divs);
console.log(isArray); // true
People sometimes use this technique to get the “true” type of an object as well. For example, Array
, Function
, and RegExp
objects all shadow (override) the Object.prototype.toString()
method. We can use the Function.prototype.call
method to get around this:
let foo = [];
let type = Object.prototype.toString.call(foo);
console.log(type); // "[object Array]"
foo = function () {};
type = Object.prototype.toString.call(foo);
console.log(type); // "[object Function]"
foo = /(?:)/;
type = Object.prototype.toString.call(foo);
console.log(type); // "[object RegExp]"
These are just a few examples, but hopefully you can appreciate the versatility of this technique. However, if you just want to invoke a method on an object’s prototype, I would argue that the super.method()
syntax makes your intention clearer.