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.
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]) 30Explanation: 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.
new Date(...[2011, 11, 24]) // Christmas Eve 2011However, 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 constructornew 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.
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:
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)
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).