Destruktor der Klasse ECMAScript 6

74

Ich weiß, dass ECMAScript 6 Konstruktoren hat, aber gibt es Destruktoren für ECMAScript 6?

Wenn ich beispielsweise einige Methoden meines Objekts als Ereignis-Listener im Konstruktor registriere, möchte ich sie entfernen, wenn mein Objekt gelöscht wird.

Eine Lösung besteht darin, eine Konvention zu erstellen desctructor, bei der für jede Klasse, die dieses Verhalten benötigt , eine Methode erstellt und manuell aufgerufen wird. Dadurch werden die Verweise auf die Ereignishandler entfernt, sodass mein Objekt wirklich für die Speicherbereinigung bereit ist. Andernfalls bleibt es aufgrund dieser Methoden im Speicher.

Ich hatte jedoch gehofft, dass ECMAScript 6 etwas Natives enthält, das aufgerufen wird, bevor das Objekt durch Müll gesammelt wird.

Wenn es keinen solchen Mechanismus gibt, was ist ein Muster / eine Konvention für solche Probleme?

AlexStack
quelle
2
Ich würde nicht sagen, dass ein Destruktor der richtige Ort dafür wäre. Müllsammler sammeln keinen Speicher, wenn Sie es erwarten, daher glaube ich, dass eine manuelle Entsorgung immer noch erforderlich ist.
Matías Fidemraizer
3
Wenn Sie Ereignis-Listener haben, kann Ihr Objekt erst dann GC-geprüft werden, wenn sie verschwunden sind. Es gibt kein Szenario, in dem eine solche Funktion nützlich wäre.
SLaks
Danke Jungs. Aber was wäre eine gute Konvention, wenn ECMAScript keine Destruktoren hätte? Sollte ich eine aufgerufene Methode erstellen destructorund manuell aufrufen, wenn ich mit dem Objekt fertig bin? Irgendeine andere Idee?
AlexStack
Teilen Sie den Abschnitt zur Bindung von Event-Handlern in zwei Methoden auf: this.subscribe () und this.unsubscribe ()
dandavis

Antworten:

34

Gibt es Destruktoren für ECMAScript 6?

Nein. EcmaScript 6 spezifiziert überhaupt keine Garbage Collection-Semantik [1] , daher gibt es auch nichts Besseres als eine "Zerstörung".

Wenn ich einige Methoden meines Objekts als Ereignis-Listener im Konstruktor registriere, möchte ich sie entfernen, wenn mein Objekt gelöscht wird

Ein Destruktor würde dir hier nicht einmal helfen. Es sind die Ereignis-Listener selbst, die immer noch auf Ihr Objekt verweisen, sodass es nicht in der Lage ist, Müll zu sammeln, bevor sie nicht registriert sind.
Was Sie tatsächlich suchen, ist eine Methode zum Registrieren von Listenern, ohne sie als Live-Root-Objekte zu markieren. (Fragen Sie Ihren lokalen Hersteller von Veranstaltungsquellen nach einer solchen Funktion.)

1): Nun, es gibt einen Anfang mit der Spezifikation von WeakMapund WeakSetObjekten. Es sind jedoch noch echte schwache Referenzen in Vorbereitung [1] [2] .

Bergi
quelle
13
Dam, ich freue mich auf diese echten Javascript-Destruktoren.
Pacerier
1
Dies wäre äußerst hilfreich. So etwas wie in Reagieren, wie es sein wird ObjektWillUnmount
Jason Sebring
@nurettin Das hat nichts mit Destruktoren zu tun?! (Übrigens verwenden Sie ES8 async/ await, nicht ES9 Promise.prototype.finally.)
Bergi
37

Ich bin gerade bei einer Suche nach Destruktoren auf diese Frage gestoßen und dachte, dass Ihre Kommentare einen unbeantworteten Teil Ihrer Frage enthalten, also dachte ich, ich würde das ansprechen.

Danke Jungs. Aber was wäre eine gute Konvention, wenn ECMAScript keine Destruktoren hätte? Sollte ich eine Methode namens destructor erstellen und manuell aufrufen, wenn ich mit dem Objekt fertig bin? Irgendeine andere Idee?

Wenn Sie Ihrem Objekt mitteilen möchten, dass Sie damit fertig sind und es speziell alle Ereignis-Listener freigeben soll, die es hat, können Sie einfach eine gewöhnliche Methode dafür erstellen. Sie können wie die Methode etwas rufen release()oder deregister()oder unhook()oder irgendetwas dieser Sorte. Die Idee ist, dass Sie das Objekt anweisen, sich von allem anderen zu trennen, an das es angeschlossen ist (Listener von Ereignissen abmelden, externe Objektreferenzen löschen usw.). Sie müssen es zum richtigen Zeitpunkt manuell aufrufen.

Wenn Sie gleichzeitig sicherstellen, dass keine anderen Verweise auf dieses Objekt vorhanden sind, kann Ihr Objekt zu diesem Zeitpunkt nicht mehr mit Speicherbereinigung belegt werden.

ES6 verfügt zwar über schwaches Karten- und schwaches Set, mit denen Sie eine Reihe von Objekten verfolgen können, die noch am Leben sind, ohne zu beeinflussen, wann sie mit Müll gesammelt werden können. Sie bieten jedoch keine Benachrichtigung, wenn sie mit Müll gesammelt werden. Sie verschwinden einfach irgendwann aus der schwachMap oder dem schwachen Satz (wenn sie GCed sind).


Zu Ihrer Information, das Problem mit dieser Art von Destruktor, nach dem Sie fragen (und wahrscheinlich, warum es nicht viel Aufruf dafür gibt), ist, dass ein Element aufgrund der Speicherbereinigung nicht für die Speicherbereinigung berechtigt ist, wenn es einen offenen Ereignishandler hat ein lebendes Objekt. Selbst wenn es einen solchen Destruktor gäbe, würde es unter Ihren Umständen niemals aufgerufen werden, bis Sie die Ereignis-Listener tatsächlich entfernt haben. Sobald Sie die Ereignis-Listener entfernt haben, ist der Destruktor für diesen Zweck nicht mehr erforderlich.

Ich nehme an, es gibt eine Möglichkeit weakListener(), die die Speicherbereinigung nicht verhindern würde, aber so etwas gibt es auch nicht.


Zu Ihrer Information, hier ist eine weitere relevante Frage: Warum fehlt das Objektzerstörer-Paradigma in den von Müll gesammelten Sprachen allgegenwärtig? . Diese Diskussion behandelt die Entwurfsmuster von Finalisierern, Destruktoren und Entsorgern. Ich fand es nützlich, den Unterschied zwischen den drei zu sehen.


Bearbeiten im Jahr 2020 - Vorschlag für Objekt-Finalizer

Es gibt einen EMCAScript-Vorschlag der Stufe 3 zum Hinzufügen einer benutzerdefinierten Finalizer-Funktion, nachdem ein Objekt durch Müll gesammelt wurde.

Ein kanonisches Beispiel für etwas, das von einer solchen Funktion profitieren würde, ist ein Objekt, das ein Handle für eine geöffnete Datei enthält. Wenn das Objekt durch Müll gesammelt wird (da noch kein anderer Code darauf verweist), kann mit diesem Finalizer-Schema zumindest eine Nachricht an die Konsole gesendet werden, dass gerade eine externe Ressource durchgesickert ist und Code an anderer Stelle behoben werden sollte, um dies zu verhindern dieses Leck.

Wenn Sie den Vorschlag gründlich lesen, werden Sie feststellen, dass es sich nicht um einen vollständigen Destruktor in einer Sprache wie C ++ handelt. Dieser Finalizer wird aufgerufen, nachdem das Objekt bereits zerstört wurde und Sie vorab festlegen müssen, welcher Teil der Instanzdaten an den Finalizer übergeben werden muss, damit er seine Arbeit ausführen kann. Darüber hinaus ist diese Funktion nicht für den normalen Betrieb gedacht, sondern als Debugging-Hilfe und als Rückschlag gegen bestimmte Arten von Fehlern. Die vollständige Erklärung für diese Einschränkungen finden Sie im Vorschlag.

jfriend00
quelle
15

Sie müssen Objekte in JS manuell "zerstören". Das Erstellen einer Zerstörungsfunktion ist in JS üblich. In anderen Sprachen kann dies als "frei", "freigeben", "entsorgen", "schließen" usw. bezeichnet werden. Nach meiner Erfahrung handelt es sich jedoch tendenziell um "Zerstören", wodurch interne Verweise, Ereignisse und möglicherweise auch zerstörte Zerstörungsaufrufe an untergeordnete Objekte aufgehoben werden.

WeakMaps sind weitgehend nutzlos, da sie nicht iteriert werden können und wahrscheinlich erst nach ECMA 7 verfügbar sind, wenn überhaupt. Mit WeakMaps können Sie nur unsichtbare Eigenschaften vom Objekt selbst trennen, mit Ausnahme der Suche nach Objektreferenz und GC, damit sie es nicht stören. Dies kann zum Zwischenspeichern, Erweitern und Behandeln von Pluralität nützlich sein, hilft jedoch nicht wirklich bei der Speicherverwaltung für Observablen und Beobachter. WeakSet ist eine Teilmenge von WeakMap (wie eine WeakMap mit dem Standardwert boolean true).

Es gibt verschiedene Argumente, ob verschiedene Implementierungen von schwachen Referenzen für diese oder Destruktoren verwendet werden sollen. Beide haben potenzielle Probleme und Destruktoren sind begrenzter.

Destruktoren sind möglicherweise auch für Beobachter / Zuhörer potenziell nutzlos, da der Zuhörer normalerweise entweder direkt oder indirekt Verweise auf den Beobachter enthält. Ein Destruktor arbeitet wirklich nur als Proxy ohne schwache Referenzen. Wenn Ihr Observer wirklich nur ein Proxy ist, der die Listener eines anderen nimmt und sie auf ein Observable setzt, kann er dort etwas tun, aber so etwas ist selten nützlich. Destruktoren sind eher für E / A-bezogene Dinge oder für Dinge außerhalb des Umfangs des Containments gedacht (IE, der zwei von ihm erstellte Instanzen miteinander verbindet).

Der spezielle Fall, nach dem ich angefangen habe, ist, weil ich eine Klasse-A-Instanz habe, die Klasse B im Konstruktor nimmt und dann eine Klasse-C-Instanz erstellt, die auf B hört. Ich behalte die B-Instanz immer irgendwo oben. KI wird manchmal weggeworfen, neue erstellt, viele erstellt usw. In dieser Situation würde ein Destruktor tatsächlich für mich arbeiten, aber mit einem bösen Nebeneffekt, der im übergeordneten Element, wenn ich die C-Instanz weitergab, aber alle A-Referenzen entfernte, dann die C- und Die B-Bindung würde unterbrochen (bei C wird der Boden darunter entfernt).

In JS ist es schmerzhaft, keine automatische Lösung zu haben, aber ich denke nicht, dass es leicht lösbar ist. Betrachten Sie diese Klassen (Pseudo):

function Filter(stream) {
    stream.on('data', function() {
        this.emit('data', data.toString().replace('somenoise', '')); // Pretend chunks/multibyte are not a problem.
    });
}
Filter.prototype.__proto__ = EventEmitter.prototype;
function View(df, stream) {
    df.on('data', function(data) {
        stream.write(data.toUpper()); // Shout.
    });
}

Nebenbei bemerkt ist es schwierig, Dinge ohne anonyme / eindeutige Funktionen zum Laufen zu bringen, die später behandelt werden.

Im Normalfall wäre die Instanziierung wie folgt (Pseudo):

var df = new Filter(stdin),
    v1 = new View(df, stdout),
    v2 = new View(df, stderr);

Um diese normalerweise zu GCen, würden Sie sie auf null setzen, aber es wird nicht funktionieren, da sie einen Baum mit stdin im Stamm erstellt haben. Dies ist im Grunde das, was Ereignissysteme tun. Sie geben einem Kind ein Elternteil, das Kind fügt sich dem Elternteil hinzu und kann dann einen Verweis auf das Elternteil beibehalten oder nicht. Ein Baum ist ein einfaches Beispiel, aber in Wirklichkeit finden Sie sich möglicherweise auch mit komplexen Graphen wieder, wenn auch selten.

In diesem Fall fügt Filter stdin einen Verweis auf sich selbst in Form einer anonymen Funktion hinzu, die indirekt auf Filter nach Bereich verweist. Bereichsreferenzen sind etwas zu beachten und das kann sehr komplex sein. Ein leistungsstarker GC kann einige interessante Dinge tun, um Elemente in Bereichsvariablen zu entfernen, aber das ist ein anderes Thema. Es ist wichtig zu verstehen, dass, wenn Sie eine anonyme Funktion erstellen und sie als Listener zu ab beobachtbar zu etwas hinzufügen, die beobachtbare Funktion einen Verweis auf die Funktion und alles behält, worauf die Funktion in den darüber liegenden Bereichen verweist (in denen sie definiert wurde) ) wird ebenfalls beibehalten. Die Ansichten machen dasselbe, aber nach der Ausführung ihrer Konstruktoren behalten die Kinder keinen Verweis auf ihre Eltern bei.

Wenn ich einige oder alle der oben deklarierten Variablen auf null setze, wird dies keinen Unterschied machen (ähnlich, wenn dieser "Haupt" -Bereich beendet ist). Sie sind weiterhin aktiv und leiten Daten von stdin nach stdout und stderr weiter.

Wenn ich sie alle auf null setze, ist es unmöglich, sie zu entfernen oder zu GCen, ohne die Ereignisse auf stdin zu löschen oder stdin auf null zu setzen (vorausgesetzt, es kann so freigegeben werden). Sie haben im Grunde genommen einen Speicherverlust auf diese Weise mit tatsächlich verwaisten Objekten, wenn der Rest des Codes stdin benötigt und andere wichtige Ereignisse darauf sind, die Sie daran hindern, die oben genannten Aktionen auszuführen.

Um df, v1 und v2 loszuwerden, muss ich für jeden eine Zerstörungsmethode aufrufen. In Bezug auf die Implementierung bedeutet dies, dass sowohl die Filter- als auch die View-Methode den Verweis auf die von ihnen erstellte anonyme Listener-Funktion sowie die Observable beibehalten und diese an removeListener übergeben müssen.

Nebenbei bemerkt, Sie können alternativ ein Observable haben, das einen Index zurückgibt, um die Listener zu verfolgen, sodass Sie prototypisierte Funktionen hinzufügen können, die zumindest nach meinem Verständnis in Bezug auf Leistung und Speicher viel besser sein sollten. Sie müssen jedoch den zurückgegebenen Bezeichner nachverfolgen und Ihr Objekt übergeben, um sicherzustellen, dass der Listener beim Aufruf daran gebunden ist.

Eine Zerstörungsfunktion fügt mehrere Schmerzen hinzu. Erstens müsste ich es aufrufen und die Referenz freigeben:

df.destroy();
v1.destroy();
v2.destroy();
df = v1 = v2 = null;

Dies ist ein kleiner Ärger, da es etwas mehr Code ist, aber das ist nicht das eigentliche Problem. Wenn ich diese Referenzen an viele Objekte weitergebe. Wann genau nennst du in diesem Fall zerstören? Sie können diese nicht einfach an andere Objekte weitergeben. Sie werden mit Ketten von Zerstörungen und manueller Implementierung der Verfolgung entweder durch Programmablauf oder auf andere Weise enden. Du kannst nicht schießen und vergessen.

Ein Beispiel für diese Art von Problem ist, wenn ich beschließe, dass View bei Zerstörung auch destroy auf df aufruft. Wenn v2 immer noch in der Nähe ist, wird die Zerstörung von df unterbrochen, sodass die Zerstörung nicht einfach an df weitergeleitet werden kann. Wenn v1 df benötigt, um es zu verwenden, müsste es df mitteilen, dass es verwendet wird, was einen Zähler oder ähnliches wie df auslösen würde. Die Zerstörungsfunktion von df würde abnehmen als der Zähler und nur dann tatsächlich zerstören, wenn sie 0 ist. Diese Art von Dingen fügt eine Menge Komplexität hinzu und fügt eine Menge hinzu, die schief gehen können. Die offensichtlichste davon ist, etwas zu zerstören, während es irgendwo noch einen Hinweis darauf gibt wird verwendet und Zirkelverweise (an diesem Punkt geht es nicht mehr darum, einen Zähler zu verwalten, sondern eine Karte der referenzierenden Objekte). Wenn Sie daran denken, Ihre eigenen Referenzzähler, MM usw. in JS zu implementieren, dann ist es '

Wenn WeakSets iterierbar wären, könnte dies verwendet werden:

function Observable() {
    this.events = {open: new WeakSet(), close: new WeakSet()};
}
Observable.prototype.on = function(type, f) {
    this.events[type].add(f);
};
Observable.prototype.emit = function(type, ...args) {
    this.events[type].forEach(f => f(...args));
};
Observable.prototype.off = function(type, f) {
    this.events[type].delete(f);
};

In diesem Fall muss die besitzende Klasse auch einen Token-Verweis auf f behalten, sonst geht es schief.

Wenn Observable anstelle von EventListener verwendet würde, würde die Speicherverwaltung in Bezug auf die Ereignis-Listener automatisch erfolgen.

Anstatt "destroy" für jedes Objekt aufzurufen, würde dies ausreichen, um sie vollständig zu entfernen:

df = v1 = v2 = null;

Wenn Sie df nicht auf null setzen würden, wäre es immer noch vorhanden, aber v1 und v2 würden automatisch ausgehängt.

Bei diesem Ansatz gibt es jedoch zwei Probleme.

Problem eins ist, dass es eine neue Komplexität hinzufügt. Manchmal wollen die Leute dieses Verhalten eigentlich nicht. Ich könnte eine sehr große Kette von Objekten erstellen, die eher durch Ereignisse als durch Eindämmung miteinander verbunden sind (Verweise in Konstruktorbereichen oder Objekteigenschaften). Irgendwann müssten ein Baum und ich nur noch um die Wurzel herumgehen und uns darüber Sorgen machen. Das Befreien der Wurzel würde das Ganze bequem befreien. Beide Verhaltensweisen sind abhängig vom Codierungsstil usw. nützlich. Wenn Sie wiederverwendbare Objekte erstellen, ist es schwierig zu wissen, was die Benutzer wollen, was sie getan haben, was Sie getan haben und wie schwierig es ist, das zu umgehen, was getan wurde. Wenn ich Observable anstelle von EventListener verwende, muss df entweder auf v1 und v2 verweisen, oder ich muss sie alle übergeben, wenn ich das Eigentum an der Referenz auf etwas anderes außerhalb des Gültigkeitsbereichs übertragen möchte. Eine schwache Referenz würde das Problem ein wenig abmildern, indem die Kontrolle von Observable auf einen Beobachter übertragen wird, sie jedoch nicht vollständig lösen (und jede Emission oder jedes Ereignis auf sich selbst überprüfen müssen). Ich nehme an, dieses Problem kann behoben werden, wenn das Verhalten nur für isolierte Diagramme gilt, die den GC erheblich komplizieren würden, und nicht für Fälle, in denen es Referenzen außerhalb des Diagramms gibt, die in der Praxis keine Probleme sind (nur CPU-Zyklen verbrauchen, keine Änderungen vorgenommen).

Problem zwei ist, dass es entweder in bestimmten Fällen unvorhersehbar ist oder die JS-Engine zwingt, das GC-Diagramm für die Objekte bei Bedarf zu durchlaufen, was einen schrecklichen Einfluss auf die Leistung haben kann (obwohl es, wenn es klug ist, vermeiden kann, es pro Mitglied zu tun, indem es es pro macht WeakMap-Schleife stattdessen). Der GC wird möglicherweise nie ausgeführt, wenn die Speichernutzung einen bestimmten Schwellenwert nicht erreicht und das Objekt mit seinen Ereignissen nicht entfernt wird. Wenn ich v1 auf null setze, wird es möglicherweise für immer an stdout weitergeleitet. Selbst wenn es GCed wird, ist dies willkürlich, es kann für eine beliebige Zeitspanne (1 Zeilen, 10 Zeilen, 2,5 Zeilen usw.) an stdout weitergeleitet werden.

Der Grund, warum WeakMap sich nicht um den GC kümmert, wenn er nicht iterierbar ist, besteht darin, dass Sie für den Zugriff auf ein Objekt ohnehin einen Verweis darauf haben müssen, damit es entweder nicht GCed ist oder nicht zur Karte hinzugefügt wurde.

Ich bin mir nicht sicher, was ich von so etwas halte. Sie brechen die Speicherverwaltung, um sie mit dem iterierbaren WeakMap-Ansatz zu beheben. Problem zwei kann auch für Destruktoren existieren.

All dies ruft mehrere Ebenen der Hölle hervor, daher würde ich vorschlagen, zu versuchen, es mit gutem Programmdesign, guten Praktiken, Vermeidung bestimmter Dinge usw. zu umgehen. Es kann in JS jedoch frustrierend sein, weil es in bestimmten Aspekten flexibel ist und weil Es ist natürlicher asynchron und ereignisbasiert mit starker Umkehrung der Kontrolle.

Es gibt eine andere Lösung, die ziemlich elegant ist, aber auch hier noch einige potenziell schwerwiegende Probleme aufweist. Wenn Sie eine Klasse haben, die eine beobachtbare Klasse erweitert, können Sie die Ereignisfunktionen überschreiben. Fügen Sie Ihre Ereignisse nur dann zu anderen Observablen hinzu, wenn Ereignisse zu sich selbst hinzugefügt werden. Wenn alle Ereignisse von Ihnen entfernt wurden, entfernen Sie Ihre Ereignisse von untergeordneten Elementen. Sie können auch eine Klasse erstellen, um Ihre beobachtbare Klasse zu erweitern und dies für Sie zu tun. Eine solche Klasse könnte Hooks für leer und nicht leer bereitstellen, so dass Sie sich selbst beobachten würden. Dieser Ansatz ist nicht schlecht, hat aber auch Probleme. Es gibt eine Zunahme der Komplexität sowie eine Abnahme der Leistung. Sie müssen einen Verweis auf das Objekt behalten, das Sie beobachten. Kritisch ist, dass es auch bei Blättern nicht funktioniert, aber zumindest die Zwischenprodukte zerstören sich selbst, wenn Sie das Blatt zerstören. Es' s wie Verketten zerstören, aber versteckt hinter Anrufen, die Sie bereits verketten müssen. Ein großes Leistungsproblem besteht darin, dass Sie möglicherweise interne Daten aus dem Observable jedes Mal neu initialisieren müssen, wenn Ihre Klasse aktiv wird. Wenn dieser Vorgang sehr lange dauert, sind Sie möglicherweise in Schwierigkeiten.

Wenn Sie WeakMap iterieren könnten, könnten Sie möglicherweise Dinge kombinieren (zu Schwach wechseln, wenn keine Ereignisse vorliegen, Stark, wenn Ereignisse), aber alles, was wirklich getan wird, ist, das Leistungsproblem auf eine andere Person zu übertragen.

Es gibt auch sofortige Belästigungen mit iterierbarer WeakMap, wenn es um Verhalten geht. Ich habe kurz zuvor über Funktionen mit Bereichsreferenzen und Schnitzen gesprochen. Wenn ich ein untergeordnetes Element instanziiere, das im Konstruktor den Listener 'console.log (param)' mit dem übergeordneten Element verknüpft und das übergeordnete Element nicht beibehalten kann, kann es beim Entfernen aller Verweise auf das untergeordnete Element vollständig als anonyme Funktion freigegeben werden, die dem übergeordneten Element hinzugefügt wird Eltern verweisen auf nichts innerhalb des Kindes. Dies lässt die Frage offen, was mit parent.weakmap.add (child, (param) => console.log (param)) zu tun ist. Meines Wissens ist der Schlüssel schwach, aber nicht der Wert, so dass schwachmap.add (Objekt, Objekt) persistent ist. Dies ist jedoch etwas, das ich neu bewerten muss. Für mich sieht das wie ein Speicherverlust aus, wenn ich alle anderen Objektreferenzen entsorge, aber ich vermute, dass dies in Wirklichkeit gelingt, indem es als Zirkelreferenz betrachtet wird. Entweder behält die anonyme Funktion einen impliziten Verweis auf Objekte bei, die sich aus übergeordneten Bereichen ergeben, um eine Konsistenz zu erzielen, die viel Speicher verschwendet, oder Sie haben ein Verhalten, das aufgrund von Umständen variiert, die schwer vorherzusagen oder zu verwalten sind. Ich denke, Ersteres ist eigentlich unmöglich. Im letzteren Fall, wenn ich eine Methode für eine Klasse habe, die einfach ein Objekt nimmt und console.log hinzufügt, wird sie freigegeben, wenn ich die Verweise auf die Klasse lösche, selbst wenn ich die Funktion zurückgebe und eine Referenz verwalte.

jgmjgm
quelle
Gibt es eine Diskussion zwischen den ES-Planern und -Designern, um eventuell Haken in das GC-System aufzunehmen?
CMCDragonkai
4

"Ein Destruktor würde Ihnen hier nicht einmal helfen. Es sind die Ereignis-Listener selbst, die immer noch auf Ihr Objekt verweisen, sodass es nicht in der Lage ist, Müll zu sammeln, bevor sie nicht registriert sind."

Nicht so. Der Zweck eines Destruktors besteht darin, dem Element, das die Listener registriert hat, zu erlauben, die Registrierung aufzuheben. Sobald ein Objekt keine weiteren Verweise darauf hat, wird es mit Müll gesammelt.

Wenn AngularJS beispielsweise in AngularJS zerstört wird, kann er auf ein Zerstörungsereignis warten und darauf reagieren. Dies ist nicht dasselbe wie das automatische Aufrufen eines Destruktors, aber es ist nah und gibt uns die Möglichkeit, Listener zu entfernen, die beim Initialisieren des Controllers festgelegt wurden.

// Set event listeners, hanging onto the returned listener removal functions
function initialize() {
    $scope.listenerCleanup = [];
    $scope.listenerCleanup.push( $scope.$on( EVENTS.DESTROY, instance.onDestroy) );
    $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.CREATE_USER.SUCCESS, instance.onCreateUserResponse ) );
    $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.CREATE_USER.FAILURE, instance.onCreateUserResponse ) );
}

// Remove event listeners when the controller is destroyed
function onDestroy(){
    $scope.listenerCleanup.forEach( remove => remove() );
}


Cliff Hall
quelle
4

Wenn es keinen solchen Mechanismus gibt, was ist ein Muster / eine Konvention für solche Probleme?

Der Begriff "Bereinigung" ist möglicherweise angemessener, verwendet jedoch "Destruktor", um mit OP übereinzustimmen

Angenommen, Sie schreiben Javascript vollständig mit 'function's' und 'var's'. Dann können Sie das Muster des Schreibens alle verwenden functions - Code im Rahmen eines try/ catch/ finallyGitter. Führen Sie innerhalb finallyden Zerstörungscode aus.

Anstelle des C ++ - Stils, Objektklassen mit nicht angegebenen Lebensdauern zu schreiben und dann die Lebensdauer durch beliebige Bereiche und den impliziten Aufruf von ~()am Ende des Bereichs anzugeben ( ~()ist Destruktor in C ++), ist in diesem Javascript-Muster das Objekt die Funktion, der Bereich ist genau der Funktionsumfang und der Destruktor ist der finallyBlock.

Wenn Sie denken , jetzt dieses Muster von Natur aus da ist fehlerhaft try/ catch/ finallynicht encompass asynchrone Ausführung , die auf Javascript wesentlich ist, dann sind Sie richtig. Glücklicherweise wurde dem Hilfsobjekt für asynchrone Programmierung seit 2018 Promiseeine Prototypfunktion finallyzu den bereits vorhandenen resolveund catchPrototypfunktionen hinzugefügt . Das bedeutet, dass asynchrone Bereiche, die Destruktoren erfordern, mit einem PromiseObjekt geschrieben werden können, das finallyals Destruktor verwendet wird. Des Weiteren können Sie mit try/ catch/ finallyin einem async functionAufruf Promises mit oder ohne await, aber darüber im Klaren sein müssen , dassPromises, die ohne Wartezeit aufgerufen werden, werden asynchron außerhalb des Bereichs ausgeführt und behandeln den Desctructor-Code in einem Finale then.

Im folgenden Code PromiseAund PromiseBgibt einige Legacy - API - Ebene Versprechen, die keine finallyfestgelegten Funktionsargumente. PromiseCHat ein endgültiges Argument definiert.

async function afunc(a,b){
    try {
        function resolveB(r){ ... }
        function catchB(e){ ... }
        function cleanupB(){ ... }
        function resolveC(r){ ... }
        function catchC(e){ ... }
        function cleanupC(){ ... }
        ...
        // PromiseA preced by await sp will finish before finally block.  
        // If no rush then safe to handle PromiseA cleanup in finally block 
        var x = await PromiseA(a);
        // PromiseB,PromiseC not preceded by await - will execute asynchronously
        // so might finish after finally block so we must provide 
        // explicit cleanup (if necessary)
        PromiseB(b).then(resolveB,catchB).then(cleanupB,cleanupB);
        PromiseC(c).then(resolveC,catchC,cleanupC);
    }
    catch(e) { ... }
    finally { /* scope destructor/cleanup code here */ }
}

Ich befürworte nicht, dass jedes Objekt in Javascript als Funktion geschrieben wird. Stellen Sie sich stattdessen den Fall vor, in dem Sie einen Bereich identifiziert haben, der wirklich möchte, dass ein Destruktor am Ende seiner Lebensdauer aufgerufen wird. Formulieren Sie diesen Bereich als Funktionsobjekt, indem Sie den finallyBlock des Musters (oder die finallyFunktion im Fall eines asynchronen Bereichs) als Destruktor verwenden. Es ist ziemlich wahrscheinlich, dass die Formulierung dieses Funktionsobjekts die Notwendigkeit einer Nichtfunktionsklasse überflüssig machte, die sonst geschrieben worden wäre - es war kein zusätzlicher Code erforderlich, und die Ausrichtung von Umfang und Klasse könnte sogar sauberer sein.

Hinweis: Wie andere geschrieben haben, sollten wir Destruktoren und Garbage Collection nicht verwechseln. C ++ - Destruktoren befassen sich häufig oder hauptsächlich mit der manuellen Speicherbereinigung, jedoch nicht ausschließlich . Javascript erfordert keine manuelle Speicherbereinigung, aber das Ende der Lebensdauer des asynchronen Bereichs ist häufig ein Ort zum (De-) Registrieren von Ereignis-Listenern usw.

Craig Hicks
quelle