2011-08-01

JavaScript performance: Array.prototype versus []

Array.prototype contains many generic methods that can be applied to array-like objects. [] is a popular shortcut for accessing these methods. This post examines the pros and cons of using that shortcut.

Update: Inspired by a comment from Kevin Roberts, I’ve added a third way of accessing generic methods, and a conclusion.

Explanations

Array-like objects. Some objects in JavaScript are array-like, they have indexed access and a length property like arrays, but none of the array methods. Array-like objects include the special variable arguments (giving indexed access to all arguments that were passed to a function) and most DOM results. Not having the standard array methods is especially unfortunate under ECMAScript 5, which has goodies such as Array.prototype.forEach.

Generic methods. Some methods are generic. While they are directly available to instances of their prototype, they can also be borrowed by other instances. To borrow a generic method, one invokes one of the following two methods on it:

  • Function.prototype.call(thisValue, [arg1], [arg2], ...)
  • Function.prototype.apply(thisValue, [arrayWithArguments])
The borrowing instance is the first argument and becomes the value of this. Generic methods have to be written so that they require this to only have a minimal set of methods. For example, most generic array methods only need this to provide length and indexed access. Array.prototype.slice is generic and allows one to turn any part of an array-like object into an array.

Example: invoking Array.prototype.map() generically, on the array-like arguments object.

    function prefixHello(prefix) {
        return Array.prototype.map.call(arguments, function(elem) {
            return "Hello "+elem;
        });
    }
Interaction:
    > prefixHello("Jane", "John")
    [ 'Hello Jane', 'Hello John' ]

[] as a shortcut. [].foo is often used as a shortcut for Array.prototype.foo. That is, you access a prototype property via an instance.

  • Pro: More compact.
  • Con: Does not really describe one’s intent. You are not trying to invoke an instance method, you are borrowing a function from the prototype.
  • Con: Slightly slower (see below).

Timing several ways of accessing generic methods

I wanted to see how bad performance really suffered and did a quick non-scientific test. Test framework:
    var iterations = 100000000;
    var data = []; // empty so that slice() doesn’t have much to do
    (function () {
        var start = (new Date).getTime();
        
        // loop

        var diff = (new Date).getTime() - start;
        console.log(diff);    
    }());
Timing prototype access:
    for(var i=0; i<iterations; i++) {
        Array.prototype.slice.call(data);
    }
Timing the shortcut:
    for(var i=0; i<iterations; i++) {
        [].slice.call(data);
    }
Storing the prototype in a local variable:
    var arrayProto = Array.prototype;
    for(var i=0; i<iterations; i++) {
        arrayProto.slice.call(data);
    }
Results (iMac, 2.7 GHz Intel Core i5):

iterationsprototypeshortcutquick prototype
Node.js 0.4.8100,000,0005019ms5075ms4692ms
Firefox 610,000,0001592ms2237ms1522ms
Rhino 1.7 release 310,000,0002318ms2687ms1878ms

Conclusion

The time differences are not exactly earth-shattering. Thus, unless you are working with performance-critical code, you should choose whatever you think reads best (as opposed to what is easiest to write).

No comments: