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 thisValue – apply 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).
2 comments:
Wouldn't it be nice if javascript supported Function.prototype.construct as callback into "new"
Can you explain what you mean by that?
Post a Comment