2011-08-24

Spreading arrays into arguments in JavaScript

Update 2012-02-01: Complete rewrite of the section on spreading constructor arguments.

Sometimes, one needs to spread the elements of an array, to use them as the arguments of a function call. JavaScript allows you to do that via Function.prototype.apply, but that does not work for constructor invocations. This post explains spreading and how to make it work in the presence of the new operator.

Spreading

Spreading means making a function call, method call or constructor invocation and supplying the arguments via an array. In Python, one calls this unpacking. ECMAScript.next will have the spread operator (prefix ...) for this purpose. In current JavaScript, you can perform this operation via Function.prototype.apply.

Spreading function arguments

Math.max() returns the maximum among its 0 or more numeric parameters. With the spread operator, you can use it for arrays:
    Math.max(...[13, 7, 30])
This is the equivalent of
    Math.max(13, 7, 30)
In current JavaScript, you have to use apply().
    > Math.max.apply(null, [13, 7, 30])
    30
Explanation: An apply invocation looks as follows:
    func.apply(thisValue, [param1, param2, ...])
which is equivalent to
    thisValue.func(param1, param2, ...)
Note that func does not have to be a method of thisValueapply temporarily turns it into one.

Spreading constructor arguments

The Date constructor takes several numeric parameters and produces a date. With the spread operator, you can hand in an array.
    new Date(...[2011, 11, 24]) // Christmas Eve 2011
However, this time we cannot use apply to implement spreading, because it does not work with new:
    > new Date.apply(null, [2011, 11, 24])
    TypeError: function apply() { [native code] } is not a constructor
new expects Date.apply to be a constructor function. No matter how you parenthesize the above expression, the fundamental problem remains: apply performs a function call, it does not hand arguments to the new operator.

A manual work-around

First step. Let’s get the result right first, we’ll worry later about handing in an array instead of separate arguments.
    new (Date.bind(null, 2011, 11, 24))
We used bind() to produce a function with zero parameters (by providing them ahead of time) and then invoked that nullary function via new. bind has the following signature:
    func.bind(thisValue, arg1, arg2, ...)
It turns func into a fresh function whose implicit this parameter is thisValue and whose initial arguments are always as given. When one invokes the fresh function, the arguments of such an invocation are appended to what has already been provided via bind. MDN has more details. Note that in the previous example, the first argument was null, because bind turns Date into a function that does not need a thisValue: It is only invoked as a constructor and new overrides the thisValue from bind.

Second step. We want to hand an array to bind. So we again use apply to turn an array into arguments for a function call.

    new (Function.prototype.bind.apply(
        Date, [null].concat([2011, 11, 24])))
We invoked apply on the function Function.prototype.bind, with two arguments:
  • 1st argument: this has the value Date, as in Date.bind(...), above.
  • 2nd argument: The arguments for bind are created by prepending null to the array [2011, 11, 24].

A library method

Mozilla suggests turning the above work-around into a library method. The following is a slightly edited version of their suggestion:
    if (!Function.prototype.construct) {
        Function.prototype.construct = function(argArray) {
            if (! Array.isArray(argArray)) {
                throw new TypeError("Argument must be an array");
            }
            var constr = this;
            var nullaryFunc = Function.prototype.bind.apply(
                constr, [null].concat(argArray));
            return new nullaryFunc();
        };
    }
Interaction:
    > Date.construct([2011, 11, 24])
    Sat Dec 24 2011 00:00:00 GMT+0100 (CET)

A seemingly simpler solution

You can manually perform the actions of the new operator. For example:
    var foo = new Foo("abc");
Is equivalent to
    var foo = Object.create(Foo.prototype);
    Foo.call(foo, "abc");
With that work-around, we can write a simpler library method (checks are omitted):
    Function.prototype.construct = function(argArray) {
        var constr = this;
        var inst = Object.create(constr.prototype);
        constr.apply(inst, argArray);
        return inst;
    };
Alas, Date invoked as a function works the same as Date invoked as a constructor: It ignores the first parameter of call() and apply() and always produces a new instance.
    > Date.construct([2011, 11, 24])
    {}
You can see that the result of construct() has not been set up. Several built-in constructors work the same as Date. But the library method will work properly with most non-built-in constructors (handling constructors that actively return values require a bit more work).

No comments: