Gibt es eine Möglichkeit, Object.freeze () ein JavaScript-Datum zu geben?

76

Laut MDN- Object.freeze()Dokumentation :

Die Object.freeze()Methode friert ein Objekt ein, dh es wird verhindert, dass neue Eigenschaften hinzugefügt werden. verhindert, dass vorhandene Eigenschaften entfernt werden; und verhindert, dass vorhandene Eigenschaften oder deren Aufzählbarkeit, Konfigurierbarkeit oder Beschreibbarkeit geändert werden. Im Wesentlichen wird das Objekt effektiv unveränderlich gemacht. Die Methode gibt das eingefrorene Objekt zurück.

Ich hatte erwartet, dass das Aufrufen des Einfrierens an einem Datum Änderungen an diesem Datum verhindern würde, aber es scheint nicht zu funktionieren. Folgendes mache ich (mit 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)

Ich hätte erwartet, dass der Anruf setTimeentweder fehlschlägt oder nichts tut. Irgendwelche Ideen, wie man ein Date einfriert?

Andrew Eisenberg
quelle
Wir laden eine JSON-codierte Konfiguration aus einer Datei und möchten sicherstellen, dass kein anderer Teil der App versehentlich Änderungen an dieser Konfiguration vornimmt. Wir rufen also rekursiv Object.freeze()das gesamte Objekt auf. Es scheint zu funktionieren, bis auf dieses kleine Date-Problem.
Andrew Eisenberg
1
Speichern Sie das Datum als Zeichenfolge oder Zeitstempel und konvertieren Sie es vor der Verwendung in ein Datumsobjekt. Sie können auch einen Getter für dieses Zeichenfolgenobjekt erstellen, der das Datum zurückgibt. Auf diese Weise werden Sie überhaupt kein Datumsobjekt verwenden, daher würde dies kein Einfrieren des Datums erfordern
Sachin
Berücksichtigen Sie alle Funktionen, die unveränderlich sind, wenn sie ein neues Objekt zurückgeben. Ich bin mir nicht sicher, wie dies mit freeze () interagieren würde.
Owen Beresford
Dies ist ein Grund, warum ich standardmäßig Unveränderlichkeit genieße.
Ben Leggiero
1
@ BenC.R.Leggiero Einverstanden. Veränderbare Daten sind ein grundlegender Fehler von Java und JavaScript. Es verursacht kein Ende der Probleme.
Andrew Eisenberg

Antworten:

62

Gibt es eine Möglichkeit, Object.freeze () ein JavaScript-Datum zu geben?

Das glaube ich nicht. Sie können erhalten nah , aber unter der unten stehenden Zeile. Aber zuerst wollen wir sehen, warum es einfach Object.freezenicht funktioniert.

Ich hatte erwartet, dass ein Anruf beim Einfrieren eines Datums Änderungen an diesem Datum verhindern würde ...

Es wäre , wenn Date eine Objekteigenschaft zu halten seine interne Zeitwert, aber es funktioniert nicht verwendet. Stattdessen wird ein [[DateValue]] interner Steckplatz verwendet. Interne Slots sind keine Eigenschaften:

Interne Slots entsprechen dem internen Status, der Objekten zugeordnet ist und von verschiedenen ECMAScript-Spezifikationsalgorithmen verwendet wird. Interne Slots sind keine Objekteigenschaften ...

Das Einfrieren des Objekts hat also keinen Einfluss auf seine Fähigkeit, seinen [[DateValue]]internen Steckplatz zu mutieren .


Sie können a einfrieren Date, oder so effektiv: Ersetzen Sie alle seine Mutator-Methoden durch No-Op-Funktionen (oder Funktionen, die einen Fehler auslösen) und dann freeze. Aber wie von zzzzBov (nett!) Beobachtet , hindert das niemanden daran Date.prototype.setTime.call(d, 0) (in einem absichtlichen Versuch, das eingefrorene Objekt zu , oder als Nebenprodukt eines komplizierten Codes, den sie verwenden). Also ist es nah , aber keine Zigarre.

Hier ist ein Beispiel (ich verwende hier ES2015-Funktionen, da ich das letin Ihrem Code gesehen habe, daher benötigen Sie einen aktuellen Browser, um es auszuführen; dies kann jedoch auch nur mit ES5-Funktionen durchgeführt werden):

Ich denke, alle Mutator-Methoden Datebeginnen damit set, aber wenn nicht, ist es einfach, die oben genannten zu optimieren.

TJ Crowder
quelle
1
Ich wünschte, ich könnte dies zweimal positiv bewerten. Das ist sehr interessant.
Mike Cluck
2
Ha! You can freeze a Date, or effectively so anyway: Replace all its mutator methods with no-ops and then freeze it.Großartige Idee!
Andrew Eisenberg
8
"Sie können ein Date" einfrieren - ... ish, denken Sie daran, dass das Überschreiben der Funktion mit einem No-Op umgangen werden kann, indem Sie die Funktion von einem Date.prototypesolchen aus aufrufen, wie Date.prototype.setTime.call(frozenDate, 0)dies in der Praxis natürlich lächerlich wäre, aber lokale Tests zeigen, dass es funktioniert Zumindest in Chrome.
zzzzBov
2
@zzzzBov: Lächerliche Sache zu tun? Vielleicht, aber eine sehr, sehr nützliche Einschränkung .
TJ Crowder
5
Könnte es einfacher sein, nur den valueOf/ timestamp zu verwenden und diese Eigenschaft unveränderlich zu machen - und sie einfach nach DateBedarf in ein neues Objekt zu werfen ?
Abgesandter
8

Aus den DokumentenObject.freeze von MDN (Schwerpunkt Mine):

Werte können für Dateneigenschaften nicht geändert werden. Accessor-Eigenschaften (Getter und Setter) funktionieren genauso (und geben dennoch die Illusion, dass Sie den Wert ändern). Beachten Sie, dass Werte, die Objekte sind, weiterhin geändert werden können, sofern sie nicht ebenfalls eingefroren sind.

Die setTimeMethode des Date-Objekts ändert keine Eigenschaft des Date-Objekts und funktioniert daher weiterhin, obwohl die Instanz eingefroren wurde.

zzzzBov
quelle
7

Das ist eine wirklich gute Frage!

Die Antwort von TJ Crowder hat eine ausgezeichnete Lösung, aber ich habe darüber nachgedacht: Was können wir noch tun? Wie können wir das umgehen Date.prototype.setTime.call(yourFrozenDate)?

1. Versuch: "Wrapper"

Eine direkte Möglichkeit besteht darin, eine AndrewDateFunktion bereitzustellen , die ein Datum umschließt. Es hat alles, was ein Datum hat, abzüglich der Setter:

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

Dadurch wird ein Objekt erstellt, das alle Eigenschaften eines tatsächlichen Datumsobjekts aufweist und Function.prototype.bindzum Festlegen seiner Objekte verwendet this.

Dies ist keine narrensichere Art, sich um die Schlüssel zu versammeln, aber hoffentlich können Sie meine Absicht sehen.

Aber warte ... wenn wir es hier und da etwas weiter betrachten, können wir sehen, dass es einen besseren Weg gibt, dies zu tun.

2. Versuch: 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());

Und wir haben es gelöst!

... irgendwie. Siehe, Firefox ist derzeit die einzige, die Proxys implementiert, und aus bizarren Gründen können Datumsobjekte nicht als Proxys verwendet werden. Außerdem werden Sie feststellen, dass Sie immer noch Dinge wie tun können, 'setDate' in proxyDateund Sie werden Vervollständigungen in der Konsole sehen. Um dies zu überwinden, müssen mehr Fallen bereitgestellt werden. Insbesondere has, enumerate, ownKeys,getOwnPropertyDescriptor und wer weiß , was seltsam Rand Fällen gibt es!

... Beim zweiten Gedanken ist diese Antwort fast sinnlos. Aber zumindest hatten wir Spaß, oder?

Zirak
quelle
Tatsächlich implementieren Chrome (und NodeJS) Proxys in Stable. Sie können es sicher verwenden.
Benjamin Gruenbaum
gut :) Übrigens: Gibt es einen Grund, warum Sie Reflect.get anstelle von target [prop] verwenden?
Kamil Tomšík
@ KamilTomšík Symmetrie! Jeder Proxy-Trap hat seine Standardimplementierung in Reflect. Wenn Sie also Proxys implementieren und die Standardaktion delegieren, verwende ich gerne Reflect. Kein besonderer Grund anders als das.
Zirak
6

Sie können es in eine klassenähnliche Struktur einschließen und benutzerdefinierte Getter und Setter definieren, um unerwünschte Änderungen zu vermeiden

Mensur Grišević
quelle
1
Das klingt nach der einfachsten Antwort für mich
sricks
2

Die akzeptierte Antwort ist leider fehlerhaft. Sie können tatsächlich eine Instanz eines beliebigen Objekts einfrieren, einschließlich einer Instanz von Date. Zur Unterstützung der Antwort von @zzzzBov bedeutet das Einfrieren einer Objektinstanz nicht, dass der Status des Objekts konstant wird.

Ein Weg, um zu beweisen, dass eine DateInstanz wirklich eingefroren ist, besteht darin, die folgenden Schritte auszuführen:

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

Aber Sie können das Verhalten erreichen, das Sie vermutlich wie folgt wünschen:

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
quelle