mdn english developer javascript ecmascript-6

javascript - english - Destructor de clase ECMAScript 6



javascript mdn english (4)

"Un destructor ni siquiera te ayudaría aquí. Son los propios oyentes del evento los que aún hacen referencia a tu objeto, por lo que no podrían recoger basura antes de que no estén registrados".

No tan. El propósito de un destructor es permitir que el elemento que registró los oyentes los anule. Por ejemplo, en Angular, cuando se destruye un controlador, puede escuchar un evento de destrucción y responder a él. Esto no es lo mismo que tener un destructor llamado automáticamente, pero está cerca, y nos da la oportunidad de eliminar los oyentes que se establecieron cuando el controlador se inicializó.

// Initialize the controller function initialize() { // Set event listeners, hanging onto the returned listener removal functions $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 ) ); $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.RESET_PW.SUCCESS, instance.onSendResetEmailResponse ) ); $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.RESET_PW.FAILURE, instance.onSendResetEmailResponse ) ); $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.CHANGE_PW.SUCCESS, instance.onChangePasswordResponse ) ); $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.CHANGE_PW.FAILURE, instance.onChangePasswordResponse ) ); $scope.listenerCleanup.push( $scope.$on( ACCOUNT_SERVICE_RESPONSES.CREATE_PROFILE.SUCCESS, instance.onCreateProfileResponse ) ); $scope.listenerCleanup.push( $scope.$on( ACCOUNT_SERVICE_RESPONSES.CREATE_PROFILE.FAILURE, instance.onCreateProfileResponse ) ); }; } /** * Remove event listeners when the controller is destroyed */ function onDestroy(){ var i, removeListener; for (i=0; i < $scope.listenerCleanup.length; i++){ removeListener = $scope.listenerCleanup[i]; removeListener(); } }

Sé que ECMAScript 6 tiene constructores, pero ¿existe algo así como destructores para ECMAScript 6?

Por ejemplo, si registro algunos de los métodos de mi objeto como detectores de eventos en el constructor, quiero eliminarlos cuando se elimine mi objeto.

Una solución es tener una convención de crear un método desctructor para cada clase que necesite este tipo de comportamiento y llamarlo manualmente. Esto eliminará las referencias a los manejadores de eventos, por lo tanto, mi objeto estará listo para la recolección de basura. De lo contrario, permanecerá en la memoria debido a esos métodos.

Pero esperaba si ECMAScript 6 tiene algo nativo que se llamará justo antes de que el objeto sea recolectado.

Si no existe tal mecanismo, ¿qué es un patrón / convención para tales problemas?


¿Hay algo así como destructores para ECMAScript 6?

No. EcmaScript 6 no especifica ninguna semántica de recolección de basura en absoluto [1] , por lo que tampoco hay nada como una "destrucción".

Si registro algunos de los métodos de mi objeto como detectores de eventos en el constructor, quiero eliminarlos cuando se elimine mi objeto

Un destructor ni siquiera te ayudaría aquí. Son los propios oyentes del evento los que aún hacen referencia a su objeto, por lo que no podrían obtener la recolección de basura antes de que no se registren.
Lo que realmente está buscando es un método para registrar oyentes sin marcarlos como objetos raíz activos. (Consulte con su fabricante local de fuentes de eventos para obtener dicha característica).

1): Bueno, hay un comienzo con la especificación de los objetos WeakMap y WeakSet . Sin embargo, las referencias débiles verdaderas todavía están en la tubería [1] [2] .


Acabo de encontrar esta pregunta en una búsqueda sobre destructores y pensé que había una parte sin respuesta de su pregunta en sus comentarios, así que pensé que abordaría eso.

gracias chicos. Pero, ¿cuál sería una buena convención si ECMAScript no tiene destructores? ¿Debería crear un método llamado destructor y llamarlo manualmente cuando termine con el objeto? ¿Alguna otra idea?

Si desea decirle a su objeto que ya ha terminado y que debe liberar específicamente los detectores de eventos que tiene, entonces puede crear un método común para hacerlo. Puedes llamar al método algo así como release() o deregister() o unhook() o cualquier cosa de esa índole. La idea es que le diga al objeto que se desconecte de cualquier otra cosa en la que esté conectado (anular el registro de escuchas de eventos, borrar referencias externas de objetos, etc.). Tendrá que llamarlo manualmente en el momento apropiado.

Si, al mismo tiempo, también se asegura de que no haya otras referencias a ese objeto, entonces su objeto será elegible para la recolección de basura en ese punto.

ES6 tiene weakMap y weakSet, que son formas de realizar un seguimiento de un conjunto de objetos que todavía están vivos sin afectar cuando pueden ser recolectados como basura, pero no proporciona ningún tipo de notificación cuando son recolectados. Simplemente desaparecen de weakMap o weakSet en algún momento (cuando están GCed).

FYI, el problema con este tipo de destructor que pides (y probablemente por qué no hay mucha demanda) es que debido a la recolección de basura, un elemento no es elegible para la recolección de basura cuando tiene un controlador de evento abierto contra un objeto vivo, por lo que incluso si existiera un destructor de este tipo, nunca sería invocado en su circunstancia hasta que haya eliminado a los oyentes del evento. Y, una vez que haya eliminado los detectores de eventos, no hay necesidad del destructor para este fin.

Supongo que hay un posible weakListener() que no evitaría la recolección de basura, pero tal cosa tampoco existe.

FYI, aquí hay otra pregunta relevante ¿ Por qué está ausente el paradigma del destructor de objetos en los lenguajes recolectados? . Esta discusión cubre los patrones de diseño de finalizador, destructor y triturador. Me pareció útil ver la distinción entre los tres.


Tienes que "destruir" objetos manualmente en JS. Crear una función de destrucción es común en JS. En otros idiomas, esto se podría llamar libre, liberar, eliminar, cerrar, etc. En mi experiencia, aunque tiende a ser destructivo, lo que desengancha referencias internas, eventos y posiblemente propaga también destruye llamadas a objetos secundarios.

Los WeakMaps son en gran parte inútiles, ya que no se pueden repetir y es probable que esto no esté disponible hasta ECMA 7, si es que lo hace. Todo lo que WeakMaps le permite hacer es tener propiedades invisibles separadas del objeto en sí, excepto la búsqueda por la referencia del objeto y el GC para que no lo molesten. Esto puede ser útil para el almacenamiento en caché, la extensión y el tratamiento de la pluralidad, pero en realidad no ayuda con el manejo de la memoria para observadores y observadores. WeakSet es un subconjunto de WeakMap (como un WeakMap con un valor predeterminado de boolean true).

Hay varios argumentos sobre si se deben usar varias implementaciones de referencias débiles para esto o destructores. Ambos tienen problemas potenciales y los destructores son más limitados.

Los destructores son en realidad potencialmente inútiles para los observadores / oyentes porque típicamente el oyente mantendrá referencias al observador directa o indirectamente. Un destructor solo funciona de manera proxy sin referencias débiles. Si su Observer es solo un proxy que toma los Escuchadores de otra persona y los pone en un observable, entonces puede hacer algo allí, pero este tipo de cosas rara vez es útil. Los destructores son más para cosas relacionadas con IO o para hacer cosas fuera del alcance de la contención (IE, conectando dos instancias que creó).

El caso específico en el que comencé a buscar esto es porque tengo una instancia de clase A que toma la clase B en el constructor, luego crea una instancia de clase C que escucha B. Siempre guardo la instancia B en algún lugar muy arriba. AI a veces tirar, crear nuevos, crear muchos, etc. En esta situación, un Destructor realmente funcionaría para mí, pero con un desagradable efecto secundario que en el padre si pasaba la instancia C pero eliminaba todas las referencias A, entonces la C y La unión B se rompería (C tiene el suelo removido de debajo).

En JS no tener una solución automática es doloroso, pero no creo que sea fácil de resolver. Considere estas clases (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. }); }

En una nota lateral, es difícil hacer que las cosas funcionen sin funciones anónimas / únicas que se tratarán más adelante.

En una instanciación de caso normal sería así (pseudo):

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

Para GC estos normalmente los establecerías como nulos, pero no funcionarán porque crearon un árbol con stdin en la raíz. Esto es básicamente lo que hacen los sistemas de eventos. Le da un padre a un hijo, el niño se agrega al padre y luego puede o no mantener una referencia al padre. Un árbol es un ejemplo simple, pero en realidad también puede encontrarse con gráficos complejos, aunque raramente.

En este caso, Filter agrega una referencia a stdin en forma de una función anónima que hace referencia indirectamente a Filtrar por ámbito. Las referencias de alcance son algo a tener en cuenta y pueden ser bastante complejas. Un GC potente puede hacer algunas cosas interesantes para escatimar elementos en variables de ámbito, pero ese es otro tema. Lo que es fundamental comprender es que cuando se crea una función anónima y se agrega a algo como oyente para ser observable, el observable mantendrá una referencia a la función y todo lo que la función haga referencia en los ámbitos que se encuentran arriba (que se definió en ) también se mantendrá. Las vistas hacen lo mismo pero después de la ejecución de sus constructores, los niños no mantienen una referencia a sus padres.

Si configuro alguno o todos los vars declarados arriba para anularlo, no va a hacer una diferencia en nada (de manera similar cuando finalizó ese alcance "principal"). Seguirán activos y canalizarán los datos de stdin a stdout y stderr.

Si los configuro a todos como nulos, sería imposible eliminarlos o ponerlos en GC sin borrar los eventos en stdin o establecer stdin en null (suponiendo que se pueda liberar así). Básicamente tiene una pérdida de memoria de esa manera con objetos huérfanos en efecto si el resto del código necesita stdin y tiene otros eventos importantes que le prohíben hacer lo anterior.

Para deshacerse de df, v1 y v2 necesito llamar a un método de destrucción en cada uno de ellos. En términos de implementación, esto significa que los métodos Filter y View necesitan mantener la referencia a la función de escucha anónima que crean, así como a la observable, y pasarla a removeListener.

En una nota lateral, alternativamente puede tener un obserable que devuelve un índice para realizar un seguimiento de los oyentes para que pueda agregar funciones de prototipo que, al menos a mi entender, debería ser mucho mejor en el rendimiento y la memoria. Sin embargo, aún debe hacer un seguimiento del identificador devuelto y pasar su objeto para asegurarse de que el oyente está vinculado a él cuando se le llama.

Una función de destrucción agrega varios dolores. Primero es que debería llamarlo y liberar la referencia:

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

Esto es una molestia menor ya que es un poco más código pero ese no es el problema real. Cuando entrego estas referencias a muchos objetos. En este caso, ¿cuándo llamas exactamente a destruir? No puedes simplemente entregarlos a otros objetos. Terminará con cadenas de destruye y la implementación manual del seguimiento ya sea a través del flujo del programa o de algún otro modo. No puedes disparar y olvidar.

Un ejemplo de este tipo de problema es si decido que View también invocará destroy on df cuando se destruya. Si v2 todavía está destruyendo, df lo romperá, por lo que la destrucción no se puede retransmitir a df. En cambio, cuando v1 toma df para usarlo, necesitaría decirle a df si se usa, lo que elevaría algún contador o similar a df. La función de destrucción de df disminuiría en vez de contrarrestar y solo destruiría realmente si es 0. Este tipo de cosas agrega mucha complejidad y agrega muchas cosas que pueden ir mal, la más obvia de las cuales es destruir algo, mientras que todavía hay una referencia en algún lugar que se utilizarán referencias circulares (en este punto ya no se trata de administrar un contador, sino un mapa de objetos de referencia). Cuando estás pensando en implementar tus propios contadores de referencia, MM y demás en JS, es probable que sea deficiente.

Si WeakSets fuera iterable, podría usarse:

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); };

En este caso, la clase propietaria también debe mantener una referencia simbólica a f, de lo contrario irá a poof.

Si se utilizara Observable en lugar de EventListener, entonces la administración de la memoria sería automática con respecto a los oyentes del evento.

En lugar de llamar a destruir en cada objeto, esto sería suficiente para eliminarlos por completo:

df = v1 = v2 = null;

Si no estableció df como nulo, aún existiría, pero v1 y v2 se desconectarían automáticamente.

Sin embargo, hay dos problemas con este enfoque.

El problema uno es que agrega una nueva complejidad. A veces las personas realmente no quieren este comportamiento. Podría crear una cadena muy grande de objetos vinculados entre sí por eventos en lugar de contención (referencias en ámbitos de constructor o propiedades de objeto). Finalmente, un árbol y yo solo tendríamos que pasar por la raíz y preocuparnos por eso. Liberar la raíz convenientemente liberaría todo. Ambos comportamientos dependen del estilo de codificación, etc. son útiles y al crear objetos reutilizables será difícil saber qué es lo que las personas quieren, qué han hecho, qué han hecho y qué pena hacer lo que se ha hecho. Si utilizo Observable en lugar de EventListener, entonces df necesitará hacer referencia a v1 y v2 o tendré que pasarlos todos si deseo transferir la propiedad de la referencia a algo más fuera del alcance. Una referencia débil como una cosa mitigaría un poco el problema transfiriendo el control de Observable a un observador, pero no lo resolvería por completo (y necesita verificar cada emisión o evento en sí mismo). Este problema puede solucionarse, supongo, si el comportamiento solo se aplica a gráficos aislados que complicarían mucho el CG y no se aplicarían a casos en los que hay referencias fuera del gráfico que en la práctica son noops (solo consumen ciclos de CPU, no se realizan cambios).

El problema dos es que o bien es impredecible en ciertos casos o fuerza al motor JS a atravesar el gráfico GC para esos objetos bajo demanda que pueden tener un impacto de rendimiento horrible (aunque si es inteligente puede evitar hacerlo por miembro haciéndolo por WeakMap loop en su lugar). Es posible que el GC nunca se ejecute si el uso de memoria no alcanza un cierto umbral y el objeto con sus eventos no se eliminará. Si configuro v1 como nulo, puede seguir retransmitiendo a stdout para siempre. Incluso si obtiene GCed, esto será arbitrario, puede seguir retransmitiendo a stdout por cualquier cantidad de tiempo (1 línea, 10 líneas, 2.5 líneas, etc.).

La razón por la que WeakMap se sale con la suya sin preocuparse por el GC cuando no es iterable es que para acceder a un objeto, debe tener una referencia de todos modos, por lo que no se ha agregado GC o no se ha agregado al mapa.

No estoy seguro de lo que pienso sobre este tipo de cosas. Estás como rompiendo la administración de memoria para arreglarlo con el enfoque iterativo WeakMap. El problema dos también puede existir para los destructores también.

Todo esto invoca varios niveles de infierno, así que sugeriría tratar de solucionarlo con un buen diseño de programa, buenas prácticas, evitar ciertas cosas, etc. Sin embargo, puede ser frustrante en JS debido a lo flexible que es en ciertos aspectos y porque es más asíncrono y se basa en eventos con mayor inversión de control.

Hay otra solución que es bastante elegante, pero de nuevo todavía tiene algunos bloqueos potencialmente serios. Si tiene una clase que amplía una clase observable, puede anular las funciones de evento. Agregue sus eventos a otros observables solo cuando los eventos se agreguen a usted. Cuando todos los eventos se eliminan de usted, elimine sus eventos de los niños. También puede hacer una clase para extender su clase observable y hacer esto por usted. Dicha clase podría proporcionar ganchos para vacíos y no vacíos, así que en un momento en que te estarías Observando a ti mismo. Este enfoque no es malo, pero también tiene problemas. Hay un aumento de la complejidad y una disminución del rendimiento. Tendrás que mantener una referencia al objeto que observas. Críticamente, tampoco funcionará para las hojas, pero al menos los productos intermedios se autodestruirán si destruyes la hoja. Es como encadenar destruir pero oculto detrás de llamadas que ya tienes que encadenar. Sin embargo, un gran problema de rendimiento es que tendrá que reiniciar los datos internos del Observable cada vez que su clase se active. Si este proceso lleva mucho tiempo, es posible que tengas problemas.

Si pudieras iterar WeakMap, tal vez podrías combinar cosas (cambiar a Débil cuando no hay eventos, Fuerte cuando hay eventos) pero todo lo que realmente está haciendo es poner el problema del rendimiento en otra persona.

También hay molestias inmediatas con WeakMap iterativo cuando se trata de comportamiento. Mencioné brevemente antes sobre las funciones que tienen referencias de alcance y talla. Si instalo un niño que en el constructor que engancha el oyente ''console.log (param)'' al padre y no puede persistir el padre entonces cuando elimino todas las referencias al niño podría liberarse completamente como la función anónima agregada al el padre no hace referencia a nada desde dentro del niño. Esto deja la pregunta de qué hacer con parent.weakmap.add (child, (param) => console.log (param)). Que yo sepa, la clave es débil, pero no el valor, así que weakmap.add (objeto, objeto) es persistente. Esto es algo que necesito reevaluar sin embargo. Para mí eso parece una pérdida de memoria si dispongo de todas las demás referencias a objetos pero sospecho que en realidad lo logra básicamente al verlo como una referencia circular. O bien la función anónima mantiene una referencia implícita a los objetos resultantes de los ámbitos principales para perder uniformidad en la memoria o tiene un comportamiento que varía según las circunstancias que es difícil de predecir o administrar. Creo que lo primero es realmente imposible. En este último caso, si tengo un método en una clase que simplemente toma un objeto y agrega console.log, se liberaría cuando borre las referencias a la clase, incluso si devolví la función y mantuve una referencia. Para ser justos, este escenario particular raramente se necesita legítimamente, pero eventualmente alguien encontrará un ángulo y pedirá un HalfWeakMap que sea iterable (libere llaves y valores liberados) pero eso también es impredecible (obj = null termina mágicamente IO, f = nulo que termina IO mágicamente, ambos factibles a distancias increíbles).