Is there a way to Object.freeze() a JavaScript Date?
Solution 1:
Is there a way to Object.freeze() a JavaScript Date?
I don't think so. You can get close, though, see under the line below. But first let's see why just Object.freeze
doesn't work.
I was expecting that calling freeze on a date would prevent changes to that date...
It would if Date
used an object property to hold its internal time value, but it doesn't. It uses a [[DateValue]]
internal slot instead. Internal slots aren't properties:
Internal slots correspond to internal state that is associated with objects and used by various ECMAScript specification algorithms. Internal slots are not object properties...
So freezing the object doesn't have any effect on its ability to mutate its [[DateValue]]
internal slot.
You can freeze a Date
, or effectively so anyway: Replace all its mutator methods with no-op functions (or functions that throw an error) and then freeze
it. But as observed by zzzzBov (nice one!), that doesn't prevent someone from doing Date.prototype.setTime.call(d, 0)
(in a deliberate attempt to get around the frozen object, or as a byproduct of some complicated code they're using). So it's close, but no cigar.
Here's an example (I'm using ES2015 features here, since I saw that let
in your code, so you'll need a recent browser to run it; but this can be done with ES5-only features as well):
"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>
I think all the mutator methods of Date
start with set
, but if not it's easy to tweak the above.
Solution 2:
From MDN's docs on Object.freeze
(emphasis mine):
Values cannot be changed for data properties. Accessor properties (getters and setters) work the same (and still give the illusion that you are changing the value). Note that values that are objects can still be modified, unless they are also frozen.
The Date object's setTime
method isn't changing a property of the Date object, so it continues to work, despite having frozen the instance.
Solution 3:
This is a really good question!
T.J. Crowder's answer has an excellent solution, but it got me thinking: What else can we do? How can we go around the Date.prototype.setTime.call(yourFrozenDate)
?
1st attempt: "Wrapper"
One direct way is to provide an AndrewDate
function which wraps a date. It has everything a date has minus the 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 does is create an object which has all the properties that an actual date object has and uses Function.prototype.bind
to set their this
.
This isn't a fool proof way of gathering around the keys, but hopefully you can see my intention.
But wait...looking at it a little further here and there, we can see that there's a better way of doing this.
2nd attempt: 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());
And we solved it!
...sort of. See, Firefox is the only one right now which implements proxies, and for some bizarre reasons date objects can't be proxied. Furthermore, you'll notice that you can still do things like 'setDate' in proxyDate
and you'll see completions in console. To overcome that more traps need to be provided; specifically, has
, enumerate
, ownKeys
, getOwnPropertyDescriptor
and who knows what weird edge cases there are!
...So on second thought, this answer is nearly pointless. But at least we had fun, right?
Solution 4:
You could wrap it in a class like structure and define custom getters and setters in order to prevent an undesired change