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

Posted

技术标签:

【中文标题】有没有办法 Object.freeze() 一个 JavaScript 日期?【英文标题】:Is there a way to Object.freeze() a JavaScript Date? 【发布时间】:2016-04-26 17:28:14 【问题描述】:

根据MDN Object.freeze() documentation:

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

我原以为在某个日期调用 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()。除了这个棘手的日期问题之外,它似乎有效。 将日期存储为字符串或时间戳,并在使用前将其转换为日期对象,或者您可以为该字符串对象创建一个将返回日期的getter。这样你根本不会使用日期对象,所以这不需要冻结日期 考虑所有不可变的函数,因为它们返回一个新对象。不确定这将如何与 freeze() 交互。 这是我享受默认不变性的原因。 @BenC.R.Leggiero 同意。可变日期是 Java 和 javascript 的基本缺陷。它会导致无穷无尽的问题。 【参考方案1】:

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

我不这么认为。不过,您可以得到 close,请参阅下面的行。但首先让我们看看为什么 Object.freeze 不起作用。

我原以为在某个日期调用 freeze 会阻止对该日期的更改...

它会如果 Date 使用对象属性来保存其内部时间值,但它没有。它改用[[DateValue]] internal slot。 Internal slots 不是属性:

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

因此冻结对象不会对其改变其[[DateValue]] 内部槽的能力产生任何影响。


可以冻结Date,或者无论如何都有效:用无操作函数(或抛出错误的函数)替换它的所有mutator方法,然后freeze它。但正如observed by zzzzBov (好一个!),这并不能阻止某人做Date.prototype.setTime.call(d, 0)(故意试图绕过冻结的物体,或者作为他们正在使用一些复杂的代码)。所以它是接近,但没有雪茄。

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

"use strict";

let d = new Date();
freezeDate(d);
d.setTime(0);
snippet.log(d);

function nop() 


function freezeDate(d) 
  allNames(d).forEach(name => 
    if (name.startsWith("set") && typeof d[name] === "function") 
      d[name] = nop;
    
  );
  Object.freeze(d);
  return d;

function allNames(obj) 
  var names = Object.create(null); // Or use Map here
  var thisObj;
  
  for (thisObj = obj; thisObj; thisObj = Object.getPrototypeOf(thisObj)) 
    Object.getOwnPropertyNames(thisObj).forEach(name => 
      names[name] = 1;
    );
  
  return Object.keys(names);
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="//tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

认为Date 的所有修改器方法都以set 开头,但如果不是,则很容易调整上述内容。

【讨论】:

我希望我能投票两次。这很有趣。 哈! You can freeze a Date, or effectively so anyway: Replace all its mutator methods with no-ops and then freeze it. 好主意! "你可以冻结Date" - ...ish,请记住,可以通过从Date.prototype 比如Date.prototype.setTime.call(frozenDate, 0),当然在实践中这很荒谬,但本地测试表明它至少在 Chrome 中工作。 @zzzzBov:可笑的事情?也许吧,但是一个非常非常有用的警告 使用valueOf / 时间戳并将该属性呈现为不可变可能会更简单 - 并在需要时将其放入新的Date 对象中?【参考方案2】:

来自MDN's docs on Object.freeze(强调我的):

无法更改数据属性的值。 访问器属性(getter 和 setter)的工作方式相同(并且仍然会产生您正在更改值的错觉)。请注意,作为对象的值仍然可以修改,除非它们也被冻结。 p>

Date 对象的 setTime 方法不会更改 Date 对象的属性,因此尽管已冻结实例,它仍会继续工作。

【讨论】:

【参考方案3】:

这是一个非常好的问题!

T.J. Crowder's answer 有一个很好的解决方案,但它让我思考:我们还能做什么?我们如何绕过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

这不是一种在钥匙周围聚集的万无一失的方式,但希望你能明白我的意图。

但是等等……再看这里和那里,我们会发现有更好的方法来做到这一点。

第二次尝试:Proxies

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)在 stable 中实现代理。您可以放心使用它。 好一个 :) 顺便说一句:你有什么理由使用 Reflect.get 而不是 target[prop]? @KamilTomšík 对称!每个 Proxy 陷阱在 Reflect 中都有其默认实现,因此在实现代理和委托默认操作时,我喜欢使用 Reflect。除此之外没有什么特别的原因。【参考方案4】:

您可以将其包装在类结构中并定义自定义 getter 和 setter 以防止不希望的更改

【讨论】:

这听起来像是对我来说最简单的答案【参考方案5】:

恐怕,公认的答案实际上是有缺陷的。 您实际上可以冻结任何对象的实例,包括 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)

【讨论】:

【参考方案6】:

使用代理对象可能是当今最好的解决方案。基于@Zirak's answer back in 2016,我已经修改和改进了代理处理程序以获得最大的兼容性:

const noop = () => 
const dateProxyHandler = 
  get(target, prop, receiver) 
    if (prop === Symbol.toStringTag) return "Date"
    if (typeof prop === "string" && prop.startsWith("set")) return noop

    const value = Reflect.get(target, prop, receiver)
    return typeof value === "function" && prop !== "constructor"
      ? value.bind(target)
      : value
  ,


function freeze(value) 
  return value instanceof Date
    ? new Proxy(Object.freeze(new Date(Number(value))), dateProxyHandler)
    : Object.freeze(value)


const frozenDate = freeze(new Date())

frozenDate.setHours(0) // noop
frozenDate.getHours() // works :)

JSON.stringify(frozenDate) // works :)

const copiedDate = new Date(Number(frozenDate)) // works :)

Object.prototype.toString.call(frozenDate) // "[object Date]"

来源:https://gist.github.com/sirlancelot/5f1922ef01e8006ea9dda6504fc06b8e

【讨论】:

【参考方案7】:

使日期成为整数对我有用:

let date = new Date();
const integerDate = Date.parse(date);
let unchangedDate = new Date(integerDate);

【讨论】:

这没有提供问题的答案,而且你的代码也不起作用,打电话unchangedDate.setTime(0)会改变日期,你可以自己检查 对我来说确实如此。问题是:任何想法如何冻结日期? integerDate 将被冻结,直到您再次将其设为变量。 .setTime(0) 它只是一个检查器,看看你是否可以更改日期,它不适用于 integerDate。 integerDate 不是日期类型

以上是关于有没有办法 Object.freeze() 一个 JavaScript 日期?的主要内容,如果未能解决你的问题,请参考以下文章

Object.freeze与 Object.seal的区别

Object.Freeze Javascript [重复]

Object.freeze

Object.freeze(); 方法冻结一个对象。

自己封装一个Object.freeze()方法

Object.freeze()