通过调用prototype.constructor.apply来实例化JavaScript对象

54

让我先举一个我想要做的具体例子。

我有一个由年、月、日、时、分、秒和毫秒组成的数组,形式为[2008,10,8,00,16,34,254]。 我想使用以下标准构造函数实例化一个日期对象:

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

如何将我的数组传递给此构造函数以获取一个新的日期实例?[ 更新:我的问题实际上超出了这个具体示例。我想要一个通用的解决方案,适用于内置的JavaScript类,如Date、Array、RegExp等,它们的构造函数超出了我的范围。]

我正在尝试做以下事情:

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

我觉得需要在某个地方加上"new"。以上代码仅返回了当前时间,就像我调用了 "(new Date()).toString()" 一样。我也认识到我可能完全走错方向了 :)

注意:请勿使用 eval() 和逐个访问数组项。我相信我应该能够直接使用该数组。


更新:进一步实验

由于没有人能够提供有效的答案,所以我进行了更多尝试。这是一个新的发现。

我可以使用自己的类来完成这个任务:

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!

但是它不能与内置的 Date 类一起使用:

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

它也不适用于数字:

var n = new Number(42);
Number.prototype.constructor.call(n, 666);
document.write(n); // Returns 42
也许这在固有对象中不可能实现?顺便说一下,我是在测试Firefox。

回答后我发现你实际上想要的是更通用的东西,这只有在你的主题中才清楚。在问题中添加文本以突出通用解决方案而不仅仅是特定于日期的清晰性。 Translated: 在回答后我可以看到,事实上您需要的是更通用的内容,这只有在您的主题中才清晰明了。在问题中添加文本以强调通用解决方案而不仅仅是特定于日期的清晰性。 - AnthonyWJones
这就解释了为什么本地对象保留其原始内部值。我认为你提出的做法是不可能的,因为它意味着我们可以对日期进行柯里化,或者Date.apply返回一个函数(这两种情况都不是)。 - Leo
4
针对您的日期示例:new Date(Date.UTC.apply(null,arraywithtime)); - some
@some:请将此评论发布为答案。这实际上是我一直在寻找的答案。 - Ates Goral
2
@MatthewSchinckel 针对日期特定情况的简单解决方法是,对于本地时区的日期,您可以使用以下代码:Date.fromArray = function (year, month, date, hours, minutes, seconds, ms) { return new Date(year, month, date, hours || 0, minutes || 0, seconds || 0, ms || 0); } 然后使用 Date.fromArray.apply(null,arraywithtime); 进行调用。请注意,您必须至少有年、月和日,否则将得到无效日期。 - some
显示剩余4条评论
13个回答

64
我已经进行了更多的调查,并得出结论,由于Date类的实现方式,这是不可能完成的壮举
我已经检查了SpiderMonkey源代码,以查看如何实现Date。我认为这归结于以下几行代码:
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)

当 Date 被用作函数时(无论是作为 Date() 还是 Date.prototype.constructor(),它们完全相同),它默认返回当前时间的字符串表示形式,格式为本地化格式。这与传入的任何参数无关:
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

我认为在JS层面上无法规避这个问题。这也许是我在这个主题中的追求终点。

我还注意到了一些有趣的事情:

    /* 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 是一个内部值为 NaN 的日期实例,因此,

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

IE不会让我们失望。它做事情的方式有些不同,可能将内部值设置为-1,以便Date.prototype始终返回一个略早于纪元的日期。


更新

我终于深入研究了ECMA-262本身,结果发现,我试图使用的(日期对象)是根据定义不可能实现的:

15.9.2 作为函数调用的Date构造函数

当Date被作为函数而非构造函数调用时,它返回表示当前时间(UTC)的字符串。

注意 函数调用 Date(…) 与具有相同参数的对象创建表达式 new Date(…) 不等价。

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

所有参数都是可选的;任何提供的参数都会被接受,但完全被忽略。一个字符串将被创建并返回,就像通过表达式 (new Date()).toString()一样。


14
看起来现在已经可以实现原生的 Function.prototype.bind,如下代码:var d = new (Date.bind.apply(Date, [2008, 10, 8, 00, 16, 34, 254])); 可以创建一个 Date 实例。请注意不要改变原意,保持内容准确且易懂。 - Crescent Fresh
2
@CrescentFresh的解决方案适用于null:var d = new (Date.bind.apply(Date, [null, 2014, 2, 28, 23, 30])); 返回一个对象:Date {Fri Mar 28 2014 23:30:00 GMT+0100} - fpirsch

14

我不认为这种方式很优雅,但在我的测试中(FF3、Saf4、IE8),它可以工作:

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

使用以下代码替换原先的代码:

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

通过以上代码,您可以创建一个新的日期对象,并使用给定的数组参数设置日期和时间。与原先的代码相比,这个代码更加简洁,并且更容易理解。

请注意,代码中使用了HTML标记,必须按照格式要求保留这些标记。


8
这是您可能解决特定情况的方法:-
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)

然而,您正在寻找一种更普遍的解决方案。创建构造函数方法的推荐代码是将其return this

因此:

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

可以构建如下:

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

然而,并非所有的实现都按照这种方式工作,最主要的原因是原型链会出错:-
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

我已经通过使用类似于您建议的包装器解决了我的特定问题。 我仍然想知道的是,如何在内置类(例如Date)的构造函数上使用apply()。 谢谢您详细的回答! - Ates Goral

4

虽然不够优雅,但这是一个解决方案:

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)
    }
}

这个功能的工作方式应该很明显。它通过代码生成创建一个函数。这个例子为每个构造函数创建了一个固定数量的参数,但这也是有用的。大多数情况下,你至少会考虑到最大参数数量。与此处的其他一些示例相比,这也更好,因为它允许你生成代码一次,然后重复使用它。生成的代码利用了javascript的可变参数特性,这样你就可以避免命名每个参数(或在列表中拼写出来并将参数传递给你生成的函数)。下面是一个可行的例子:
var dateConstructor = new GeneratedConstructor("Date", 3)
dateConstructor.createObject( [ 1982, 03, 23 ] )

这将返回以下内容:

1982年4月23日星期五00:00:00 GMT-0800(PST)

虽然有点丑陋,但它至少方便地隐藏了混乱,并且不假设编译代码本身可以被垃圾回收(因为这可能取决于实现并且是错误的可能领域)。

祝好, Scott S. McCoy


5
new Function(string) 和 eval 不是同一种方法吗? - zachleat

3
这是如何操作的步骤:
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]);

它将与任何构造函数一起使用,不仅仅是内置的或可以兼作函数(例如Date)的构造函数。
但是它需要Ecmascript 5的.bind函数。可能会有替身无法正确工作。
顺便说一下,另一个答案建议从构造函数中返回this。这可能会使使用经典继承扩展对象非常困难,因此我认为它是反模式。

2

使用ES6语法,至少有两种方法可以实现这一点:

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}`);


1
它可以与ES6扩展运算符一起使用。 您只需:
const arr = [2018, 6, 15, 12, 30, 30, 500];
const date = new Date(...arr);

console.log(date);

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])

0

你可以使用flagrant,flagrant滥用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);
}

示例用法:

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

好好享受 =)。


-1

我知道已经过了很长时间,但我有这个问题的真正答案。这远非不可能。请参见https://gist.github.com/747650以获取通用解决方案。

var F = function(){};
F.prototype = Date.prototype;
var d = new F();
Date.apply(d, comps);

7
这段代码似乎无法使用在日期对象上: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)"。 - Ates Goral

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接