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

IT技术 javascript 反射
2021-03-05 15:37:15

让我从我正在尝试做的一个具体例子开始。

我在表单中有一个包含年、月、日、小时、分钟、秒和毫秒组件的数组[ 2008, 10, 8, 00, 16, 34, 254 ]我想使用以下标准构造函数实例化一个 Date 对象:

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

如何将数组传递给此构造函数以获取新的 Date 实例?[更新:我的问题实际上超出了这个具体例子。我想要一个通用的解决方案,用于内置 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()也不要一一访问数组项。我很确定我应该能够按原样使用该数组。


更新:进一步的实验

由于还没有人能够提出有效的答案,我已经做了更多的尝试。这是一个新发现。

我可以用我自己的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!

但它不适用于内在的 Date 类:

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

它也不适用于 Number:

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

也许这对于内在对象是不可能的?我正在用 Firefox BTW 进行测试。

6个回答

我对自己进行了更多调查,得出的结论,由于 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 用作函数时(asDate()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是一个具有内部值的 Date 实例NaN,因此,

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

IE 没有让我们失望。它做的事情有点不同,可能会设置内部值,-1以便 Date.prototype 总是返回一个稍早于纪元的日期。


更新

我终于深入研究了 ECMA-262 本身,结果证明,我想要实现的(使用 Date 对象)是——根据定义——不可能:

15.9.2 作为函数调用的日期构造函数

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

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

15.9.2.1 日期([年[, 月[, 日期[, 小时[, 分钟[, 秒[, 毫秒] ] ] ] ] ] )

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

@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}
2021-05-03 15:37:15
现在看来这在本机实现的情况下是可能的Function.prototype.bindvar d = new (Date.bind.apply(Date, [2008, 10, 8, 00, 16, 34, 254]));将产生一个Date实例。
2021-05-15 15:37:15

我几乎不会称之为优雅,但在我的测试(FF3、Saf4、IE8)中它有效:

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

取而代之的是:

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

试试这个:

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

这是您解决特定情况的方法:-

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()。谢谢你的详细回复!
2021-05-16 15:37:15

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

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(太平洋标准时间)

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

干杯,斯科特·S·麦考伊

new Function(string) 不是另一种求值方式吗?
2021-05-06 15:37:15

这是你如何做到的:

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构造函数。这会使使用经典继承扩展对象变得非常困难,所以我认为它是一种反模式。