Dispatched and direct method calls in ECMAScript 5 and 6
There are two ways to call methods in JavaScript:
- via dispatch, e.g.
obj.someMethod(arg0, arg1)
- directly, e.g.
someFunc.call(thisValue, arg0, arg1)
This blog post explains how these two work and why you will rarely call methods directly in ECMAScript 6.
Dispatched method calls versus direct method calls
Background: prototype chains
Remember that each object in JavaScript is actually a chain of one or more objects ([1] is a quick refresher of JavaScript OOP). The first object inherits properties from the later objects. For example, the prototype chain of an array ['a', 'b']
looks as follows:
- The instance, holding the elements
'a'
and'b'
Array.prototype
, the properties provided by theArray
constructorObject.prototype
, the properties provided by theObject
constructornull
(the end of the chain, so not really a member of it)
You can examine the chain via Object.getPrototypeOf()
:
> var arr = ['a', 'b'];
> var p = Object.getPrototypeOf;
> p(arr) === Array.prototype
true
> p(p(arr)) === Object.prototype
true
> p(p(p(arr)))
null
Properties in “earlier” objects override properties in “later” objects. For example, Array.prototype
provides an array-specific version of the toString()
method, overriding Object.prototype.toString()
.
> var arr = ['a', 'b'];
> Object.getOwnPropertyNames(Array.prototype)
[ 'toString', 'join', 'pop', ... ]
> arr.toString()
'a,b'
Dispatched method calls
If you look at the method call arr.toString()
you can see that it actually performs two steps
- Dispatch: In the prototype chain of
arr
, retrieve the value of the first property whose name istoString
. - Call: Call the value and set the implicit parameter
this
to the receiverarr
of the method invocation.
You can make the two steps explicit by using the call()
method of functions:
> var func = arr.toString; // dispatch
> func.call(arr) // direct call, providing a value for `this`
'a,b'
Direct method calls
There are two ways to make direct method calls in JavaScript:
Function.prototype.call(thisValue, arg0?, arg1?, ...)
Function.prototype.apply(thisValue, argArray)
Both method call
and method apply
are invoked on functions. They are different from normal function calls in that you specify a value for this
. call
provides the arguments of the method call via individual parameters, apply
provides them via an array.
One problem of invoking a method via dynamic dispatch is that the method needs to be in the prototype chain of an object. call()
enables you to call a method directly while specifying the receiver. That means that you can borrow a method from an object that is not in the current prototype chain. For example, you can borrow Object.prototype.toString
and thus apply the original, un-overridden implementation of toString
to arr
:
> Object.prototype.toString.call(arr)
'[object Array]'
Methods that work with a variety of objects (not just with instances of “their” constructor) are called generic. Speaking JavaScript has a list of all methods that are generic. The list includes most array methods and all methods of Object.prototype
(which have to work with all objects and are thus implicitly generic).
Use cases for direct method calls
Provide parameters to a method via an array
Some functions accept multiple values, but only one value per parameter. What if you want to pass the values via an array?
For example, push()
lets you destructively append several values to an array:
> var arr = ['a', 'b'];
> arr.push('c', 'd')
4
> arr
[ 'a', 'b', 'c', 'd' ]
But you can’t destructively append a whole array. You can work around that limitation by using apply()
:
> var arr = ['a', 'b'];
> Array.prototype.push.apply(arr, ['c', 'd'])
4
> arr
[ 'a', 'b', 'c', 'd' ]
Similarly, Math.max()
and Math.min()
only work for single values:
> Math.max(-1, 7, 2)
7
With apply()
, you can use them for arrays:
> Math.max.apply(null, [-1, 7, 2])
7
Convert an array-like object to an array
Some objects in JavaScript are array-like, they are almost arrays, but don’t have any of the array methods. Let’s look at two examples.
First, the special variable arguments
of functions is array-like. It has a length
and indexed access to elements.
> var args = function () { return arguments }('a', 'b');
> args.length
2
> args[0]
'a'
But arguments
isn’t an instance of Array
and does not have the method forEach()
.
> args instanceof Array
false
> args.forEach
undefined
Second, the DOM method document.querySelectorAll()
returns an instance of NodeList
.
> document.querySelectorAll('a[href]') instanceof NodeList
true
> document.querySelectorAll('a[href]').forEach
undefined
Thus, for many complex operations, you need to convert array-like objects to arrays first. That is achieved via Array.prototype.slice()
. This method copies the elements of its receiver into a new array:
> var arr = ['a', 'b'];
> arr.slice()
[ 'a', 'b' ]
> arr.slice() === arr
false
If you call slice()
directly, you can convert a NodeList
to an array:
var domLinks = document.querySelectorAll('a[href]');
var links = Array.prototype.slice.call(domLinks);
links.forEach(function (link) {
console.log(link);
});
And you can convert arguments
to an array:
function format(pattern) {
// params start at arguments[1], skipping `pattern`
var params = Array.prototype.slice.call(arguments, 1);
return params;
}
console.log(format('a', 'b', 'c')); // ['b', 'c']
Use hasOwnProperty()
safely
obj.hasOwnProperty('prop')
tells you whether obj
has the own (non-inherited) property prop
.
> var obj = { prop: 123 };
> obj.hasOwnProperty('prop')
true
> 'toString' in obj // inherited
true
> obj.hasOwnProperty('toString') // own
false
However, calling hasOwnProperty
via dispatch can cease to work properly if Object.prototype.hasOwnProperty
is overridden.
> var obj1 = { hasOwnProperty: 123 };
> obj1.hasOwnProperty('toString')
TypeError: Property 'hasOwnProperty' is not a function
hasOwnProperty
may also be unavailable via dispatch if Object.prototype
is not in the prototype chain of an object.
> var obj2 = Object.create(null);
> obj2.hasOwnProperty('toString')
TypeError: Object has no method 'hasOwnProperty'
In both cases, the solution is to make a direct call to hasOwnProperty
:
> var obj1 = { hasOwnProperty: 123 };
> Object.prototype.hasOwnProperty.call(obj1, 'hasOwnProperty')
true
> var obj2 = Object.create(null);
> Object.prototype.hasOwnProperty.call(obj2, 'toString')
false
Avoiding intermediate objects
Applying an array method such as join()
to a string normally involves two steps:
var str = 'abc';
var arr = str.split(''); // step 1
var joined = arr.join('-'); // step 2
console.log(joined); // a-b-c
Strings are array-like and can become the this
value of generic array methods. Therefore, a direct call lets you avoid step 1:
var str = 'abc';
var joined = Array.prototype.join.call(str, '-');
Similarly, you can apply map()
to a string either after you split it or via a direct method call:
> function toUpper(x) { return x.toUpperCase() }
> 'abc'.split('').map(toUpper)
[ 'A', 'B', 'C' ]
> Array.prototype.map.call('abc', toUpper)
[ 'A', 'B', 'C' ]
Note that the direct calls may be more efficient, but they are also much less elegant. Be sure that they are really worth it!
Abbreviations for Object.prototype
and Array.prototype
You can access the methods of Object.prototype
via an empty object literal (whose prototype is Object.prototype
). For example, the following two direct method calls are equivalent:
Object.prototype.hasOwnProperty.call(obj, 'propKey')
{}.hasOwnProperty.call(obj, 'propKey')
The same trick works for Array.prototype
:
Array.prototype.slice.call(arguments)
[].slice.call(arguments)
This pattern has become quite popular. It does not reflect the intention of the author as clearly as the longer version, but it’s much less verbose. Speed-wise, there isn’t much of a difference between the two versions.
Alternatives to direct method calls in ECMAScript 6
Thanks to new features in ECMAScript 6, you’ll rarely need direct method calls.
The spread operator (...
) mostly replaces apply()
Making a direct method call via apply()
only because you want to turn an array into arguments is clumsy, which is why ECMAScript 6 has the spread operator (...
) for this. It provides this functionality even in dipatched method calls.
> Math.max(...[-1, 7, 2])
7
Another example:
> let arr = ['a', 'b'];
> arr.push(...['c', 'd'])
4
> arr
[ 'a', 'b', 'c', 'd' ]
As a bonus, spread also works with the new
operator:
> new Date(...[2011, 11, 24])
Sat Dec 24 2011 00:00:00 GMT+0100 (CET)
Note that apply()
can’t be used with new
– the above feat can only be achieved via a complicated work-around [2] in ECMAScript 5.
Array-like objects are less burdensome in ECMAScript 6
On one hand, ECMAScript 6 has Array.from()
, a simpler way of converting array-like objects to arrays:
let domLinks = document.querySelectorAll('a[href]');
let links = Array.from(domLinks);
links.forEach(function (link) {
console.log(link);
});
On the other hand, you won’t need the array-like arguments
, because ECMAScript 6 has rest parameters (declared via a triple dot):
function format(pattern, ...params) {
return params;
}
console.log(format('a', 'b', 'c')); // ['b', 'c']
hasOwnProperty()
hasOwnProperty()
is mostly used to implement maps via objects. Thankfully, ECMAScript 6 has a built-in Map
data structure, which means that you’ll need hasOwnProperty()
less.
Avoiding intermediate objects
Array.from()
can convert and map in a single step, if you provide it with a callback as the second argument.
> Array.from('abc', ch => ch.toUpperCase())
[ 'A', 'B', 'C' ]
As a reminder, the two step solution is:
> 'abc'.split('').map(function (x) { return x.toUpperCase() })
[ 'A', 'B', 'C' ]
Further reading
- Understanding the four layers of JavaScript OOP
- “apply() for Constructors” (Speaking JavaScript)
- “Generic Methods: Borrowing Methods from Prototypes” (Speaking JavaScript)
- JavaScript’s “this”: how it works, where it can trip you up
Comments
Post a Comment