2014-07-27

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:

  1. The instance, holding the elements 'a' and 'b'
  2. Array.prototype, the properties provided by the Array constructor
  3. Object.prototype, the properties provided by the Object constructor
  4. null (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

  1. Dispatch: In the prototype chain of arr, retrieve the value of the first property whose name is toString.
  2. Call: Call the value and set the implicit parameter this to the receiver arr 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

  1. Understanding the four layers of JavaScript OOP
  2. apply() for Constructors” (Speaking JavaScript)
  3. Generic Methods: Borrowing Methods from Prototypes” (Speaking JavaScript)
  4. JavaScript’s “this”: how it works, where it can trip you up

No comments: