78

According to MDN Object.freeze() documentation:

The Object.freeze() method freezes an object: that is, prevents new properties from being added to it; prevents existing properties from being removed; and prevents existing properties, or their enumerability, configurability, or writability, from being changed. In essence the object is made effectively immutable. The method returns the object being frozen.

I was expecting that calling freeze on a date would prevent changes to that date, but it does not appear to be working. Here's what I am doing (running 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)

I would have expected the call to setTime to either fail or do nothing. Any ideas how to freeze a date?

Michał Perłakowski
  • 88,409
  • 26
  • 156
  • 177
Andrew Eisenberg
  • 28,387
  • 9
  • 92
  • 148
  • We are loading JSON-encoded configuration from a file and want to make sure that no other part of the app accidentally makes changes to this configuration. So, we are recursively calling `Object.freeze()` on the entire object. It seems to work, except for this niggly Date problem. – Andrew Eisenberg Jan 20 '16 at 18:31
  • 1
    store date as a string or timestamp, and convert it to date object before using, or you can make a getter for that string object which will return date. that way you won't be using date object at all, so this wouldnt require freezing date – Sachin Jan 20 '16 at 18:36
  • Consider all the functions which are immutable as they return a new object. Not sure how this would interact with freeze(). – Owen Beresford Jan 20 '16 at 23:31
  • This is a reason I enjoy immutability-by-default. – Ky - Feb 04 '16 at 15:55
  • 1
    @BenC.R.Leggiero Agreed. Mutable dates is a fundamental flaw of both Java and JavaScript. It causes no end of problems. – Andrew Eisenberg Feb 04 '16 at 17:38

7 Answers7

63

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);
console.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) {
    const names = Object.create(null); // Or use Map here

    for (let 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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    I wish I could upvote this twice. That's very interesting. – Mike Cluck Jan 20 '16 at 18:10
  • 2
    Ha! `You can freeze a Date, or effectively so anyway: Replace all its mutator methods with no-ops and then freeze it.` Great idea! – Andrew Eisenberg Jan 20 '16 at 18:20
  • 8
    "You *can* freeze a `Date`" - ...ish, just remember that overwriting the function with a no-op can be circumvented by calling the function from the `Date.prototype` such as `Date.prototype.setTime.call(frozenDate, 0)`, of course in practice this would be ridiculous, but local testing shows it working at least in Chrome. – zzzzBov Jan 20 '16 at 18:20
  • Good point. However, I'm not looking to avoid malicious attempts to circumvent freezing. So, I think this would be good enough. – Andrew Eisenberg Jan 20 '16 at 18:23
  • 3
    @zzzzBov: Ridiculous thing to do? Maybe, but a *very, very useful caveat*. – T.J. Crowder Jan 20 '16 at 18:23
  • 5
    Might be simpler just to use the `valueOf` / timestamp and render that property immutable - and just throw it into a new `Date` object as and when you need it? – Emissary Jan 20 '16 at 18:26
  • Instead of a no-op, you could even make it a throw-op if the object is intended for use it in strict code. – Bergi Jan 21 '16 at 02:42
  • @zzzzBov And if you start going crazy and deleting that, you can always create an iframe and use the function from a different global object. – gsnedders Jan 21 '16 at 03:30
  • Neat answer! After reading it I tried tackling the problem in a bit of a different way: http://stackoverflow.com/a/34935141/617762 if you're interested – Zirak Jan 21 '16 at 22:01
8

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.

zzzzBov
  • 174,988
  • 54
  • 320
  • 367
8

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?

Community
  • 1
  • 1
Zirak
  • 38,920
  • 13
  • 81
  • 92
  • Actually, Chrome (and NodeJS) implement proxies in stable. You can safely use it. – Benjamin Gruenbaum Jan 27 '16 at 12:12
  • good one :) BTW: is there any reason why you're using Reflect.get instead of target[prop]? – Kamil Tomšík Aug 02 '18 at 15:27
  • @KamilTomšík Symmetry! Each Proxy trap has its default implementation in Reflect, so when implementing proxies and delegating the default action, I like to use Reflect. No special reason other than that. – Zirak Aug 11 '18 at 11:50
7

You could wrap it in a class like structure and define custom getters and setters in order to prevent an undesired change

2

The accepted answer is actually flawed, I'm afraid. You actually can freeze an instance of any object including an instance of Date. In support of @zzzzBov's answer, freezing an object instance does not imply the object's state becomes constant.

One way to prove that a Date instance is truly frozen is by following the steps below:

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 the behaviour I suppose you desire as follows:

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)
Community
  • 1
  • 1
Igwe Kalu
  • 14,286
  • 2
  • 29
  • 39
0

Using a Proxy object is likely to be the best solution nowadays. Based on @Zirak's answer back in 2016, I've revised and improved the Proxy handler for maximum compatibility:

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]"

Source: https://gist.github.com/sirlancelot/5f1922ef01e8006ea9dda6504fc06b8e

matpie
  • 17,033
  • 9
  • 61
  • 82
-1

Making the date an integear is working for me:

let date = new Date();
const integerDate = Date.parse(date);
let unchangedDate = new Date(integerDate);
  • 1
    This does not provide an answer to the question, and also your code does not work, caling `unchangedDate.setTime(0)` will change the date, you can check it your self – phoenixstudio Feb 13 '21 at 15:06
  • To me it does. The question is: Any ideas how to freeze a date? The integerDate is frozen until you make it a variable again. The .setTime(0) it just a checker to see if you can change the date, and it is not working on integerDate. – Timelimit Feb 13 '21 at 20:25
  • `integerDate` is not a Date type – matpie Apr 17 '21 at 22:08