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

And*_*erg 75 javascript immutability node.js

根据MDN Object.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)
Run Code Online (Sandbox Code Playgroud)

我本以为setTime要么失败要么无所事事.任何想法如何冻结日期?

T.J*_*der 62

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

我不这么认为.你可以近距离接触下面的行.但首先让我们看看为什么Object.freeze不起作用.

我原以为冻结日期会阻止对该日期的更改......

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

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

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


无论如何,你可以冻结一个Date或者有效地解冻它:用无操作函数(或抛出错误的函数)替换它的所有mutator方法然后freeze它.但正如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);
}
Run Code Online (Sandbox Code Playgroud)
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="//tjcrowder.github.io/simple-snippets-console/snippet.js"></script>
Run Code Online (Sandbox Code Playgroud)

认为所有的mutator方法都是Date从一开始的set,但如果不是很容易调整上面的内容.

  • "你*可以*冻结`Date`" - ... ish,只要记住用no-op覆盖函数可以通过调用`Date.prototype`中的函数来避开,例如`Date.prototype.setTime .call(frozenDate,0)`,当然在实践中这将是荒谬的,但本地测试显示它至少在Chrome中有效. (8认同)
  • 可能更简单的只是使用`valueOf`/timestamp并将该属性呈现为不可变的 - 并在需要时将其抛入一个新的`Date`对象? (5认同)
  • 哈!`你可以冻结一个日​​期,或者无论如何都有效:用无操作替换所有的mutator方法,然后冻结它.好主意! (2认同)
  • @zzzzBov:可笑的事情要做什么?也许,但是*非常非常有用的警告*. (2认同)

zzz*_*Bov 8

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

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

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


Men*_*vić 6

您可以将它包装在类结构中,并定义自定义getter和setter以防止意外更改


Zir*_*rak 6

这是一个非常好的问题!

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
Run Code Online (Sandbox Code Playgroud)

这样做是创建一个对象,它具有实际日期对象具有的所有属性并用于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());
Run Code Online (Sandbox Code Playgroud)

我们解决了!

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

......所以第二个想法,这个答案几乎毫无意义.但至少我们玩得很开心吧?