Is there a way for Object.freeze () to have a JavaScript date?

According to MDN Object.freeze() documentation :

The Object.freeze() method measures the object: this means that new properties are not added to it; prevents deletion of existing properties; and prevents changes to existing properties or their enumeration, customizability, or writeability. Essentially, an object becomes effectively immutable. The method returns a frozen object.

I expected that calling a date freeze would prevent this date from changing, but it does not seem to work. Here is what I am doing (Node.js v5.3.0 works):

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

I would expect a call to setTime result in an error or not do anything. Any ideas on how to freeze a date?

+73
javascript immutability
Jan 20 '16 at 18:04
source share
5 answers

Is there a way for Object.freeze () to have a JavaScript date?

I do not think so. You can get closer, although see below. But first, let's see why Object.freeze just Object.freeze n't work.

I expected calling a freeze on a date would prevent changes to that date ...

It would be if Date used the property of the object to store its internal time value, but it is not. Instead, it uses the [[DateValue]] internal slot . Internal slots are not properties:

Internal slots correspond to the internal state associated with objects and used by various algorithms of the ECMAScript specification. Internal slots are not object properties ...

So freezing an object does not affect its ability to mutate its internal slot [[DateValue]] .




You can freeze Date either effectively one way or another: Replace all of its mutator methods with no-op functions (or functions that cause an error), and then freeze it. But like the zzzzBov observable (good!), This doesn't stop anyone from executing Date.prototype.setTime.call(d, 0) (in a deliberate attempt to bypass a frozen object or as a by-product of some kind of complex code that they use). So it's close, but no cigar.

Here is an example (I use ES2015 functions here, since I saw this let in your code, so you need the latest browser to launch it, but this can be done using only ES5 functions):

 "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.imtqy.com/simple-snippets-console/snippet.js"></script> 

I think all mutator Date methods start with set , but if it's not easy to customize the above.

+60
Jan 20 '16 at 18:08
source share

From the MDN docs to Object.freeze (highlighted by me):

Values ​​cannot be changed for data properties. Accessor properties (receivers and setters) work the same way (and yet give the illusion that you are changing the value). Note that values ​​that are objects can still be changed if they are also not frozen.

The method of the Date object setTime does not change the property of the Date object, so it continues to work, even though it froze the instance.

+8
Jan 20 '16 at 18:08
source share

You can wrap it in a structure-like class and define custom getters and setters to prevent unwanted changes

+6
Jan 20 '16 at 18:40
source share

This is a really good question!

TJ Crowder's answer has a great solution, but it made me think: what else can we do? How can we get around Date.prototype.setTime.call(yourFrozenDate) ?

First try: "Wrapper"

One direct way is to provide an AndrewDate function that completes the date. It has everything that a date has minus setters:

 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 

What this means is to create an object that has all the properties that the actual date object has and uses Function.prototype.bind to set them to this .

This is not a stupid way to collect keys, but hopefully you can see my intent.

But wait ... looking at it a little further and here, we can see that there is a better way to do this.

Second try: Proxy

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

And we decided it!

... view. See, Firefox is now the only one that implements proxies, and for some bizarre reasons, date objects cannot be approximated. In addition, you will notice that you can still do things like 'setDate' in proxyDate , and you will see improvements on the console. To overcome this, more traps must be provided; in particular, has , enumerate , ownKeys , getOwnPropertyDescriptor and who knows what strange edge cases there are!

... So, after thinking, this answer is almost meaningless. But at least we had fun, right?

+6
Jan 21 '16 at 21:59
source share

The accepted answer is actually erroneous, I'm afraid. In fact, you can freeze an instance of any object, including a Date instance. . In response to @zzzzBov, a response freezing an instance of an object does not mean that the state of the object becomes permanent.

One way to prove that the Date instance is really frozen is by following these steps:

 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 

But you can achieve behavior that I believe you desire:

 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) 
+2
Jan 28 '16 at 23:27
source share



All Articles