有没有办法Object.freeze()一个JavaScript Date?


76

根据MDNObject.freeze()文档

Object.freeze()方法冻结对象:即,防止向其添加新属性;防止删除现有属性;并防止更改现有属性或其可枚举性,可配置性或可写性。本质上,对象实际上是不可变的。该方法返回被冻结的对象。

我期望在某个日期调用冻结会阻止对该日期进行更改,但是它似乎没有用。这是我正在做的事情(运行Node.js v5.3.0):

let d = new Date()
Object.freeze(d)
d.setTime(0)
console.log(d) // Wed Dec 31 1969 16:00:00 GMT-0800 (PST)

我本来希望致电setTime失败或不采取任何行动。任何想法如何冻结日期?


我们正在从文件加载JSON编码的配置,并希望确保应用程序的其他部分不会意外更改此配置。因此,我们递归地调用Object.freeze()整个对象。似乎可行,除了这个小小的日期问题。
Andrew Eisenberg

1
将日期存储为字符串或时间戳,并在使用前将其转换为日期对象,或者您可以为该字符串对象创建一个将返回日期的吸气剂。这样,您根本就不会使用日期对象,因此不需要冻结日期
Sachin

考虑所有返回新对象时不可变的函数。不知道这将如何与freeze()交互。
Owen Beresford

这就是我喜欢默认不变性的原因。
Ben Leggiero

1
@ BenC.R.Leggiero同意。可变日期是Java和JavaScript的根本缺陷。它不会导致任何问题。
安德鲁·艾森伯格

Answers:


62

有没有办法Object.freeze()一个JavaScript Date?

我不这么认为。但是,您可以关闭,请参见下面的行。但是首先让我们看看为什么只是Object.freeze行不通。

我期望在某个日期调用冻结会阻止对该日期进行更改...

它将如果 Date使用一个对象的属性,以保持其内部时间价值,但事实并非如此。它使用[[DateValue]] 内部插槽代替。内部插槽不是属性:

内部插槽对应于与对象关联并由各种ECMAScript规范算法使用的内部状态。内部插槽不是对象属性...

因此,冻结对象对其变异[[DateValue]]内部插槽的能力没有任何影响。


无论如何,您都可以冻结Date,或有效地冻结它:用no-op函数(或引发错误的函数)替换其所有mutator方法,然后再替换freeze。但是,正如zzzzBov (不错的一个!)观察到那样 ,这并不能阻止某人这样做(故意绕过冻结的对象,或者作为他们使用的一些复杂代码的副产品)。所以很近,但是没有雪茄。Date.prototype.setTime.call(d, 0)

这是一个示例(我在这里使用ES2015功能,因为我let在您的代码中看到了它,因此您将需要使用最新的浏览器来运行它;但这也可以通过仅ES5的功能来完成):

认为,所有以mutatorDate开头的mutator方法都set可以轻松进行上述调整。


1
我希望我能两次投票赞成。那很有趣。
迈克·克拉克

2
哈!You can freeze a Date, or effectively so anyway: Replace all its mutator methods with no-ops and then freeze it.很好的主意!
Andrew Eisenberg

8
“您可以冻结Date” -... ish,只需记住,可以通过从Date.prototype诸如这样调用来避免使用no-op覆盖函数Date.prototype.setTime.call(frozenDate, 0),当然,这在实践中是很荒谬的,但是本地测试表明它可以正常工作至少在Chrome中
zzzzBov

2
@zzzzBov:可笑的事吗?也许,但是非常有用的警告
TJ Crowder

5
使用valueOf/时间戳并使该属性变为不可变,可能会更简单-Date在需要时将其扔到新对象中?
Emissary

8

来自(强调我的)MDN的文档Object.freeze

不能更改数据属性的值。访问器属性(获取器和设置器)的工作原理相同(并且仍然给人一种幻想,即您正在更改值)。注意作为对象的值仍然可以修改,除非它们也被冻结。

Date对象的setTime方法不会更改Date对象的属性,因此即使冻结了实例,它仍然可以正常工作。


7

这是一个非常好的问题!

TJ Crowder的答案有一个很好的解决方案,但它让我思考:我们还能做什么?我们该如何解决Date.prototype.setTime.call(yourFrozenDate)

第一次尝试:“包装器”

一种直接的方法是提供一个AndrewDate包装日期的函数。它具有日期减去设置者的所有内容:

function AndrewDate(realDate) {
    var proto = Date.prototype;
    var propNames = Object.getOwnPropertyNames(proto)
        .filter(propName => !propName.startsWith('set'));

    return propNames.reduce((ret, propName) => {
        ret[propName] = proto[propName].bind(realDate);
        return ret;
    }, {});
}

var date = AndrewDate(new Date());
date.setMonth(2); // TypeError: d.setMonth is not a function

这是创建一个具有实际日期对象具有的所有属性并用于Function.prototype.bind设置其属性的对象this

这不是一个愚蠢的方法来收集密钥,但是希望您能看到我的意图。

但是,等等...再四处看看,我们可以看到有更好的方法可以做到这一点。

第二次尝试:代理

function SuperAndrewDate(realDate) {
    return new Proxy(realDate, {
        get(target, prop) {
            if (!prop.startsWith('set')) {
                return Reflect.get(target, prop);
            }
        }
    });
}

var proxyDate = SuperAndrewDate(new Date());

我们解决了!

...有点。可以看到,Firefox是目前唯一实现代理的,出于某些奇怪的原因,无法代理日期对象。此外,您会注意到您仍然可以做类似的事情,'setDate' in proxyDate并且您会在控制台中看到完成情况。为了克服这一点,需要提供更多的陷阱;具体而言,hasenumerateownKeysgetOwnPropertyDescriptor,谁知道是什么奇怪的边缘情况下,有!

...因此,再三考虑,这个答案几乎没有意义。但是至少我们玩得开心,对吗?


实际上,Chrome(和NodeJS)可以稳定地实现代理。您可以放心使用它。
本杰明·格伦鲍姆

好人:) BTW:您使用Reflect.get代替target [prop]有什么理由吗?
卡米尔·汤姆斯卡(KamilTomšík),

@KamilTomšík对称!每个Proxy陷阱在Reflect中都有其默认实现,因此在实现代理并委派默认操作时,我喜欢使用Reflect。除此之外,没有其他特殊原因。
Zirak


2

恐怕,已接受的答案实际上是有缺陷的。实际上,您可以冻结任何对象的实例,包括的实例Date为了支持@zzzzBov的答案,冻结对象实例并不意味着该对象的状态变为常量。

证明Date实例确实被冻结的一种方法是遵循以下步骤:

var date = new Date();
date.x = 4;
console.log(date.x); // 4
Object.freeze(date);
date.x = 20; // this assignment fails silently, freezing has made property x to be non-writable
date.y = 5; // this also fails silently, freezing ensures you can't add new properties to an object
console.log(date.x); // 4, unchanged
console.log(date.y); // undefined

但是您可以实现我期望的行为,如下所示:

var date = (function() {
    var actualDate = new Date();

    return Object.defineProperty({}, "value", {
        get: function() {
            return new Date(actualDate.getTime())
        },
        enumerable: true
    });
})();

console.log(date.value); // Fri Jan 29 2016 00:01:20 GMT+0100 (CET)
date.value.setTime(0);
console.log(date.value); // Fri Jan 29 2016 00:01:20 GMT+0100 (CET)
date.value = null;       // fails silently
console.log(date.value); // Fri Jan 29 2016 00:01:20 GMT+0100 (CET)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.