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

时间:2016-01-20 18:04:28

标签: javascript node.js immutability

根据MDN Object.freeze() documentation

  

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的调用失败或什么也不做。有关如何冻结日期的任何想法吗?

7 个答案:

答案 0 :(得分:61)

  

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

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

  

我原以为在某个日期冻结呼叫会阻止对该日期的更改......

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

  

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

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

可以冻结Date,或者无论如何都是有效的:用无操作函数(或抛出错误的函数)替换所有的mutator方法然后freeze它。但正如observed (很好的一个!) 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>
&#13;
&#13;
&#13;

认为 Date的所有mutator方法都以set开头,但如果没有,则很容易调整上述内容。

答案 1 :(得分:8)

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

  

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

Date对象的setTime方法不会更改Date对象的属性,所以它会继续工作,尽管已经冻结了实例。

答案 2 :(得分:6)

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

答案 3 :(得分:6)

这是一个非常好的问题!

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

第一次尝试:&#34; Wrapper&#34;

一种直接的方法是提供一个包含日期的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以及谁知道有什么奇怪的边缘情况!

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

答案 4 :(得分: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)

答案 5 :(得分:0)

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

Select 
  (e.FirstName+ ' '+ e.LastName) as EmployeeName,
  od.OrderID,
  od.ProductID,
  od.UnitPrice,
  od.Quantity,
  (od.UnitPrice * od.Quantity) as TotalSales,
  od.Discount
from 
dbo.[Order Details] od
    inner join dbo.Orders as o on od.OrderID = o.OrderID
    inner join dbo.Employees e on o.EmployeeID = e.EmployeeID

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

答案 6 :(得分:-1)

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

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