¿Hay alguna forma de Object.freeze () una fecha de JavaScript?

76

Según la documentación de MDNObject.freeze() :

El Object.freeze()método congela un objeto: es decir, evita que se le agreguen nuevas propiedades; evita que se eliminen las propiedades existentes; e impide que se modifiquen las propiedades existentes, o su enumerabilidad, configurabilidad o capacidad de escritura. En esencia, el objeto se vuelve efectivamente inmutable. El método devuelve el objeto que se congela.

Esperaba que llamar a congelar en una fecha evitaría cambios en esa fecha, pero parece que no funciona. Esto es lo que estoy haciendo (ejecutando 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)

Hubiera esperado que la llamada setTimefallara o no hiciera nada. ¿Alguna idea de cómo congelar una cita?

Andrew Eisenberg
fuente
Estamos cargando una configuración codificada en JSON desde un archivo y queremos asegurarnos de que ninguna otra parte de la aplicación realice cambios accidentalmente en esta configuración. Entonces, estamos invocando de forma recursiva Object.freeze()todo el objeto. Parece funcionar, excepto por este problema de fecha.
Andrew Eisenberg
1
almacene la fecha como una cadena o marca de tiempo y conviértala en un objeto de fecha antes de usarla, o puede crear un captador para ese objeto de cadena que devolverá la fecha. de esa manera, no usará el objeto de fecha en absoluto, por lo que esto no requeriría la fecha de congelación
Sachin
Considere todas las funciones que son inmutables cuando devuelven un nuevo objeto. No estoy seguro de cómo interactuaría esto con freeze ().
Owen Beresford
Esta es una razón por la que disfruto de la inmutabilidad por defecto.
Ben Leggiero
1
@ BenC.R.Leggiero estuvo de acuerdo. Las fechas mutables son un defecto fundamental tanto de Java como de JavaScript. Causa un sinfín de problemas.
Andrew Eisenberg

Respuestas:

62

¿Hay alguna forma de Object.freeze () una fecha de JavaScript?

No lo creo. Usted puede conseguir cerca , sin embargo, ver debajo de la línea de abajo. Pero primero veamos por qué simplemente Object.freezeno funciona.

Esperaba que llamar a congelar en una fecha evitaría cambios en esa fecha ...

Lo haría si se Date usara una propiedad de objeto para mantener su valor de tiempo interno, pero no es así. En su lugar, utiliza una [[DateValue]] ranura interna . Las ranuras internas no son propiedades:

Las ranuras internas corresponden al estado interno que está asociado con los objetos y utilizado por varios algoritmos de especificación ECMAScript. Las ranuras internas no son propiedades de objeto ...

Por lo tanto, congelar el objeto no tiene ningún efecto sobre su capacidad para mutar su [[DateValue]]ranura interna.


Usted puede congelar una Dateo eficacia de todos modos: Reemplazar todos sus métodos mutadores con funciones no-op (o funciones que arrojan un error) y luego freezeTI. Pero, como se observa por zzzzBov (agradable uno!) , Que no impide a alguien de hacerDate.prototype.setTime.call(d, 0) (en un intento deliberado de moverse por el objeto congelado, o como un subproducto de un código complicado que está utilizando). Asi que esta cerca , pero no puro.

Aquí hay un ejemplo (estoy usando las funciones de ES2015 aquí, ya que vi eso leten su código, por lo que necesitará un navegador reciente para ejecutarlo; pero esto también se puede hacer con las funciones exclusivas de ES5):

Creo que todos los métodos de mutación para Dateempezar set, pero si no, es fácil modificar los anteriores.

TJ Crowder
fuente
1
Ojalá pudiera votar a favor de esto dos veces. Eso es muy interesante.
Mike Cluck
2
¡Decir ah! You can freeze a Date, or effectively so anyway: Replace all its mutator methods with no-ops and then freeze it.¡Gran idea!
Andrew Eisenberg
8
"Usted puede congelar una Date" - ... ish, sólo recuerda que sobrescribir la función con un no-op puede evitarse llamando a la función de la Date.prototypetales como Date.prototype.setTime.call(frozenDate, 0), por supuesto, en la práctica esto sería ridículo, pero muestra de pruebas locales que trabajan al menos en Chrome.
zzzzBov
2
@zzzzBov: ¿Qué ridículo hacer? Quizás, pero una advertencia muy, muy útil .
TJ Crowder
5
¿Podría ser más simple usar la valueOfmarca de tiempo / y hacer que esa propiedad sea inmutable, y simplemente lanzarla a un nuevo Dateobjeto cuando lo necesite?
Emisario
8

De los documentos de MDN enObject.freeze (énfasis mío):

Los valores no se pueden cambiar para las propiedades de los datos. Las propiedades de los accesos (captadores y definidores) funcionan igual (y aún dan la ilusión de que está cambiando el valor). Tenga en cuenta que los valores que son objetos aún se pueden modificar, a menos que también estén congelados.

El setTimemétodo del objeto Date no cambia una propiedad del objeto Date, por lo que continúa funcionando, a pesar de haber congelado la instancia.

zzzzBov
fuente
7

¡Esta es una muy buena pregunta!

La respuesta de TJ Crowder tiene una excelente solución, pero me hizo pensar: ¿Qué más podemos hacer? ¿Cómo podemos rodear elDate.prototype.setTime.call(yourFrozenDate) ?

1er intento: "Wrapper"

Una forma directa es proporcionar una AndrewDatefunción que envuelva una fecha. Tiene todo lo que tiene una fecha menos los 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

Lo que hace es crear un objeto que tiene todas las propiedades que tiene un objeto de fecha real y utiliza Function.prototype.bindpara establecer suthis .

Esta no es una forma infalible de reunir las llaves, pero espero que puedas ver mi intención.

Pero espera ... mirándolo un poco más aquí y allá, podemos ver que hay una mejor manera de hacer esto.

2do intento: 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());

¡Y lo solucionamos!

...algo así como. Mira, Firefox es el único en este momento que implementa proxies, y por algunas razones extrañas, los objetos de fecha no se pueden usar. Además, notará que aún puede hacer cosas como 'setDate' in proxyDatey verá finalizaciones en la consola. Para superar eso, es necesario proporcionar más trampas; específicamente, has, enumerate, ownKeys,getOwnPropertyDescriptor y quién sabe qué extraños casos extremos hay!

... Entonces, pensándolo bien, esta respuesta es casi inútil. Pero al menos nos divertimos, ¿no?

Zirak
fuente
En realidad, Chrome (y NodeJS) implementan proxies en stable. Puede usarlo con seguridad.
Benjamin Gruenbaum
bueno :) Por cierto: ¿hay alguna razón por la que estás usando Reflect.get en lugar de target [prop]?
Kamil Tomšík
@ KamilTomšík Symmetry! Cada trampa de proxy tiene su implementación predeterminada en Reflect, por lo que al implementar proxies y delegar la acción predeterminada, me gusta usar Reflect. No hay otra razón especial que no sea esa.
Zirak
6

Puede envolverlo en una estructura similar a una clase y definir captadores y definidores personalizados para evitar un cambio no deseado

Mensur Grišević
fuente
1
Esto me suena como la respuesta más simple
sricks
2

La respuesta aceptada es en realidad defectuosa, me temo. De hecho, puede congelar una instancia de cualquier objeto, incluida una instancia de Date. En apoyo de @zzzzBov , congelar una instancia de objeto no implica que el estado del objeto se vuelva constante.

Una forma de demostrar que una Dateinstancia está realmente congelada es siguiendo los pasos a continuación:

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

Pero puede lograr el comportamiento que supongo que desea de la siguiente manera:

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)
Igwe Kalu
fuente