Instantiating a JavaScript object by calling prototype.constructor.apply
Asked Answered
D

13

54

Let me start with a specific example of what I'm trying to do.

I have an array of year, month, day, hour, minute, second and millisecond components in the form [ 2008, 10, 8, 00, 16, 34, 254 ]. I'd like to instantiate a Date object using the following standard constructor:

new Date(year, month, date [, hour, minute, second, millisecond ])

How can I pass my array to this constructor to get a new Date instance? [ Update: My question actually extends beyond this specific example. I'd like a general solution for built-in JavaScript classes like Date, Array, RegExp, etc. whose constructors are beyond my reach. ]

I'm trying to do something like the following:

var comps = [ 2008, 10, 8, 00, 16, 34, 254 ];
var d = Date.prototype.constructor.apply(this, comps);

I probably need a "new" in there somewhere. The above just returns the current time as if I had called "(new Date()).toString()". I also acknowledge that I may be completely in the wrong direction with the above :)

Note: No eval() and no accessing the array items one by one, please. I'm pretty sure I should be able to use the array as is.


Update: Further Experiments

Since no one has been able to come up with a working answer yet, I've done more playing around. Here's a new discovery.

I can do this with my own class:

function Foo(a, b) {
    this.a = a;
    this.b = b;

    this.toString = function () {
        return this.a + this.b;
    };
}

var foo = new Foo(1, 2);
Foo.prototype.constructor.apply(foo, [4, 8]);
document.write(foo); // Returns 12 -- yay!

But it doesn't work with the intrinsic Date class:

var d = new Date();
Date.prototype.constructor.call(d, 1000);
document.write(d); // Still returns current time :(

Neither does it work with Number:

var n = new Number(42);
Number.prototype.constructor.call(n, 666);
document.write(n); // Returns 42

Maybe this just isn't possible with intrinsic objects? I'm testing with Firefox BTW.

Depone answered 8/10, 2008 at 4:32 Comment(9)
Having answered I can see that in fact you are after something more generic which is only clear in you subject. Add text to the question to highlight that a general solution not just specific to date is clear.Calcine
Couldn't you just write Foo.apply() on the first snippet in "further experiments". I thought Foo == Foo.prototype.constructorGoldeneye
It doesn't make a difference. I get the same result.Depone
Yes, but removing the extra stuff makes it easier to see that the foo object before is the same object reference as foo after it - you're essentially just re-initializing a and b by doing the equivalent of foo.Foo = Foo; foo.Foo(4, 8) The object is still the same though.Goldeneye
That would explain why the native objects keep their original internal values. I don't think it's possible to do what you're proposing, it implies that we can either curry Date, or that Date.apply returns a function (neither of which are the case)Goldeneye
For your specific example with dates: new Date(Date.UTC.apply(null,arraywithtime));Grenville
@some: Please post this comment as an answer. This was actually the answer I was looking all along.Depone
@some: with the caveat that you'll need to adjust for timezone, as seen in Cowboy Ben Alman's answer, below.Tomfoolery
@MatthewSchinckel A simple workaround for the specific case with dates, you could use, for dates in the local time zone: Date.fromArray = function (year, month, date, hours, minutes, seconds, ms) { return new Date(year, month, date, hours || 0, minutes || 0, seconds || 0, ms || 0); } and then call it with Date.fromArray.apply(null,arraywithtime);. Note that you must have at least the year, month and day or you will get an invalid date.Grenville
D
64

I've done more investigation of my own and came up with the conclusion that this is an impossible feat, due to how the Date class is implemented.

I've inspected the SpiderMonkey source code to see how Date was implemented. I think it all boils down to the following few lines:

static JSBool
Date(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
    jsdouble *date;
    JSString *str;
    jsdouble d;

    /* Date called as function. */
    if (!(cx->fp->flags & JSFRAME_CONSTRUCTING)) {
        int64 us, ms, us2ms;
        jsdouble msec_time;

        /* NSPR 2.0 docs say 'We do not support PRMJ_NowMS and PRMJ_NowS',
         * so compute ms from PRMJ_Now.
         */
        us = PRMJ_Now();
        JSLL_UI2L(us2ms, PRMJ_USEC_PER_MSEC);
        JSLL_DIV(ms, us, us2ms);
        JSLL_L2D(msec_time, ms);

        return date_format(cx, msec_time, FORMATSPEC_FULL, rval);
    }

    /* Date called as constructor. */
    // ... (from here on it checks the arg count to decide how to create the date)

When Date is used as a function (either as Date() or Date.prototype.constructor(), which are exactly the same thing), it defaults to returning the current time as a string in the locale format. This is regardless of any arguments that are passed in:

alert(Date()); // Returns "Thu Oct 09 2008 23:15:54 ..."
alert(typeof Date()); // Returns "string"

alert(Date(42)); // Same thing, "Thu Oct 09 2008 23:15:54 ..."
alert(Date(2008, 10, 10)); // Ditto
alert(Date(null)); // Just doesn't care

I don't think there's anything that can be done at the JS level to circumvent this. And this is probably the end of my pursuit in this topic.

I've also noticed something interesting:

    /* Set the value of the Date.prototype date to NaN */
    proto_date = date_constructor(cx, proto);
    if (!proto_date)
        return NULL;
    *proto_date = *cx->runtime->jsNaN;

Date.prototype is a Date instance with the internal value of NaN and therefore,

alert(Date.prototype); // Always returns "Invalid Date"
                       // on Firefox, Opera, Safari, Chrome
                       // but not Internet Explorer

IE doesn't disappoint us. It does things a bit differently and probably sets the internal value to -1 so that Date.prototype always returns a date slightly before epoch.


Update

I've finally dug into ECMA-262 itself and it turns out, what I'm trying to achieve (with the Date object) is -- by definition -- not possible:

15.9.2 The Date Constructor Called as a Function

When Date is called as a function rather than as a constructor, it returns a string representing the current time (UTC).

NOTE The function call Date(…) is not equivalent to the object creation expression new Date(…) with the same arguments.

15.9.2.1 Date ( [ year [, month [, date [, hours [, minutes [, seconds [, ms ] ] ] ] ] ] ] )

All of the arguments are optional; any arguments supplied are accepted but are completely ignored. A string is created and returned as if by the expression (new Date()).toString().

Depone answered 19/10, 2008 at 21:58 Comment(2)
It appears this is now possible where a native implementation of Function.prototype.bind exists: var d = new (Date.bind.apply(Date, [2008, 10, 8, 00, 16, 34, 254])); will yield a Date instance.Rappee
@CrescentFresh 's solution works with a null : var d = new (Date.bind.apply(Date, [null, 2014, 2, 28, 23, 30])); returns an object : Date {Fri Mar 28 2014 23:30:00 GMT+0100}Spoonful
D
14

I'd hardly call this elegant, but in my testing (FF3, Saf4, IE8) it works:

var arr = [ 2009, 6, 22, 10, 30, 9 ];

Instead of this:

var d = new Date( arr[0], arr[1], arr[2], arr[3], arr[4], arr[5] );

Try this:

var d = new Date( Date.UTC.apply( window, arr ) + ( (new Date()).getTimezoneOffset() * 60000 ) );

Deletion answered 8/10, 2008 at 4:32 Comment(0)
C
8

This is how you might solve the specific case:-

function writeLn(s)
{
    //your code to write a line to stdout
    WScript.Echo(s)
}

var a =  [ 2008, 10, 8, 00, 16, 34, 254 ]

var d = NewDate.apply(null, a)

function NewDate(year, month, date, hour, minute, second, millisecond)
{
    return new Date(year, month, date, hour, minute, second, millisecond);
}

writeLn(d)

However you are looking for a more general solution. The recommended code for creating a constructor method is to have it return this.

Hence:-

function Target(x , y) { this.x = x, this.y = y; return this; }

could be constructed :-

var x = Target.apply({}, [1, 2]);

However not all implementations work this way not least because the prototype chain would be wrong:-

var n = {};
Target.prototype = n;
var x = Target.apply({}, [1, 2]);
var b = n.isPrototypeOf(x); // returns false
var y = new Target(3, 4);
b = n.isPrototypeOf(y); // returns true
Calcine answered 8/10, 2008 at 7:30 Comment(1)
I had already solved my specific problem by using a wrapper similar to what you suggested. What I'm still wondering is, how I can use apply() on the constructor of a built-in class (like Date). Thanks for the detailed response though!Depone
L
4

It's less than elegant, but here's a solution:

function GeneratedConstructor (methodName, argumentCount) {
    var params = []

    for (var i = 0; i < argumentCount; i++) {
        params.push("arguments[" + i + "]")
    }

    var code = "return new " + methodName + "(" + params.join(",") +  ")"

    var ctor = new Function(code)

    this.createObject = function (params) {
        return ctor.apply(this, params)
    }
}

The way this works should be pretty obvious. It creates a function through code generation. This example has a fixed number of parameters for each constructor you create, but that's useful anyway. Most of the time you have atleast a maximum number of arguments in mind. This also is better than some of the other examples here because it allows you to generate the code once and then re-use it. The code that's generated takes advantage of the variable-argument feature of javascript, this way you can avoid having to name each parameter (or spell them out in a list and pass the arguments in to the function you generate). Here's a working example:

var dateConstructor = new GeneratedConstructor("Date", 3)
dateConstructor.createObject( [ 1982, 03, 23 ] )

This will return the following:

Fri Apr 23 1982 00:00:00 GMT-0800 (PST)

It is indeed still...a bit ugly. But it atleast conveniently hides the mess and doesn't assume that compiled code itself can get garbage collected (since that may depend on the implementation and is a likely area for bugs).

Cheers, Scott S. McCoy

Larrainelarrie answered 8/10, 2008 at 4:32 Comment(1)
Isn't new Function(string) just another way to eval?Coquelicot
A
3

This is how you do it:

function applyToConstructor(constructor, argArray) {
    var args = [null].concat(argArray);
    var factoryFunction = constructor.bind.apply(constructor, args);
    return new factoryFunction();
}

var d = applyToConstructor(Date, [2008, 10, 8, 00, 16, 34, 254]);

It will work with any constructor, not just built-ins or constructors that can double as functions (like Date).

However it does require the Ecmascript 5 .bind function. Shims will probably not work correctly.

By the way, one of the other answers suggests returning this out of a constructor. That can make it very difficult to extend the object using classical inheritance, so I would consider it an antipattern.

Aspen answered 8/10, 2008 at 4:32 Comment(0)
E
2

With ES6 syntax, there's at least 2 methods to achieve this:

var comps = [ 2008, 10, 8, 00, 16, 34, 254 ];

// with the spread operator
var d1 = new Date(...comps);

// with Reflect.construct
var d2 = Reflect.construct(Date, comps);

console.log('d1:', d1, '\nd2:', d2);
// or more readable:
console.log(`d1: ${d1}\nd2: ${d2}`);
Eben answered 8/10, 2008 at 4:32 Comment(0)
I
1

It will work with ES6 spread operator. You simply:

const arr = [2018, 6, 15, 12, 30, 30, 500];
const date = new Date(...arr);

console.log(date);
Irish answered 8/10, 2008 at 4:32 Comment(0)
C
0
function gettime()
{
    var q = new Date;
    arguments.length && q.setTime( ( arguments.length === 1
        ? typeof arguments[0] === 'number' ? arguments[0] : Date.parse( arguments[0] )
        : Date.UTC.apply( null, arguments ) ) + q.getTimezoneOffset() * 60000 );
    return q;
};

gettime(2003,8,16)

gettime.apply(null,[2003,8,16])
Caster answered 8/10, 2008 at 4:32 Comment(0)
T
0

You can do it with flagrant, flagrant abuse of eval:

var newwrapper = function (constr, args) {
  var argHolder = {"c": constr};
  for (var i=0; i < args.length; i++) {
    argHolder["$" + i] = args[i];
  }

  var newStr = "new (argHolder['c'])(";
  for (var i=0; i < args.length; i++) {
    newStr += "argHolder['$" + i + "']";
    if (i != args.length - 1) newStr += ", ";
  }
  newStr += ");";

  return eval(newStr);
}

sample usage:

function Point(x,y) {
    this.x = x;
    this.y = y;
}
var p = __new(Point, [10, 20]);
alert(p.x); //10
alert(p instanceof Point); //true

enjoy =).

Tegular answered 8/10, 2008 at 4:32 Comment(0)
W
-1

Here is another solution:

function createInstance(Constructor, args){
    var TempConstructor = function(){};
    TempConstructor.prototype = Constructor.prototype;
    var instance = new TempConstructor;
    var ret = Constructor.apply(instance, args);
    return ret instanceof Object ? ret : instance;
}

console.log( createInstance(Date, [2008, 10, 8, 00, 16, 34, 254]) )
Which answered 8/10, 2008 at 4:32 Comment(0)
B
-1

I know it's been a long time, but I have the real answer to this question. This is far from impossible. See https://gist.github.com/747650 for a generic solution.

var F = function(){};
F.prototype = Date.prototype;
var d = new F();
Date.apply(d, comps);
Babara answered 8/10, 2008 at 4:32 Comment(1)
This doesn't seem to work with Date objects: function F() {}; F.prototype = Date.prototype; var d = new F(); Date.apply(d, [ 2008, 10, 8, 00, 16, 34, 254 ]); -> "Tue Apr 05 2011 13:40:43 GMT-0400 (Eastern Daylight Time)" (now)Depone
P
-2

Edited

Sorry, I was sure I made it that way years ago, right now I'll stick to:

var d = new Date(comps[0],comps[1],comps[2],comps[3],comps[4],comps[5],comps[6]);

Edit:

But do remember that a javascript Date-object uses indexes for months, so the above array means

November 8 2008 00:16:34:254

Pelham answered 8/10, 2008 at 7:50 Comment(2)
Did you test this? Gives me NaN.Calcine
The question is, how I can use the array directly, without indexing its item individually.Depone
H
-3
var comps = [ 2008, 10, 8, 00, 16, 34, 254 ];
var d = eval("new Date(" + comps.join(",") + ");");
Hyperbola answered 8/10, 2008 at 20:11 Comment(1)
I'm looking for a solution that doesn't use eval().Depone

© 2022 - 2024 — McMap. All rights reserved.