Деструктор класса ECMAScript 6

Я знаю, что ECMAScript 6 имеет конструкторы, но есть ли такая вещь, как деструкторы для ECMAScript 6?

Например, если я зарегистрирую некоторые из моих объектных методов как прослушиватели событий в конструкторе, я хочу удалить их, когда мой объект будет удален.

Одно из решений состоит в том, чтобы иметь соглашение о создании метода desctructor для каждого класса, которому требуется такое поведение, и его вручную вызвать. Это приведет к удалению ссылок на обработчики событий, поэтому мой объект будет действительно готов к сбору мусора. В противном случае он останется в памяти из-за этих методов.

Но я надеялся, что если ECMAScript 6 имеет что-то родное, которое будет вызываться прямо перед тем, как объект будет собран мусором.

Если такой механизм отсутствует, что такое шаблон/соглашение для таких проблем?

Ответ 1

Есть ли такая вещь, как деструкторы для ECMAScript 6?

Нет. EcmaScript 6 не определяет семантику сбора мусора вообще [1] поэтому нет ничего похожего на "разрушение".

Если я зарегистрирую некоторые из моих объектных методов как прослушиватели событий в конструкторе, я хочу удалить их, когда мой объект удален.

Деструктор вам даже не поможет. Это сами слушатели событий, которые все еще ссылаются на ваш объект, поэтому он не сможет собрать мусор, прежде чем они будут незарегистрированы.
То, что вы на самом деле ищете, - это метод регистрации слушателей без маркировки их как живых корневых объектов. (Спросите своего локального производителя источников событий для такой функции).

1): Ну, есть начало со спецификацией WeakMap и WeakSet. Однако истинные слабые ссылки все еще находятся в стадии разработки [1] [2].суб >

Ответ 2

Я только что наткнулся на этот вопрос в поиске деструкторов, и я подумал, что в ваших комментариях была оставлена ​​неотвеченная часть вашего вопроса, поэтому я подумал, что рассмотрю это.

Благодарю вас, ребята. Но что было бы хорошим соглашением, если ECMAScript не имеет деструкторов? Должен ли я создать метод, называемый деструктор и назовите его вручную, когда я закончил с объектом? Любая другая идея?

Если вы хотите сообщить своему объекту, что вы сейчас это сделали, и он должен специально освободить любые прослушиватели событий, которые он имеет, тогда вы можете просто создать обычный метод для этого. Вы можете вызвать метод как release() или deregister() или unhook() или что-нибудь подобное. Идея заключается в том, что вы говорите объекту, чтобы отключить себя от всего, что связано с ним (отменить регистрацию событий, очистить ссылки на внешние объекты и т.д.). Вам нужно будет вызвать его вручную в соответствующее время.

Если в то же время вы также убедитесь, что нет других ссылок на этот объект, тогда ваш объект будет иметь право на сбор мусора в этой точке.

В ES6 есть weakMap и weakSet, которые являются способами отслеживания набора объектов, которые все еще живы, не затрагивая, когда они могут быть собраны мусором, но не предоставляют никакого уведомления, когда они собираются в мусор. Они просто исчезают из weakMap или weakSet в какой-то момент (когда они GCed).


FYI, проблема с этим типом деструктора, о котором вы просите (и, вероятно, почему его не так много) заключается в том, что из-за сбора мусора элемент не имеет права на сбор мусора, когда он имеет открытую обработчик события против живого объекта, поэтому даже если бы существовал такой деструктор, он никогда не будет вызван в ваших обстоятельствах, пока вы фактически не удалите слушателей событий. И, как только вы удалите прослушиватели событий, для этой цели нет необходимости в деструкторе.

Я предполагаю, что существует возможный weakListener(), который не предотвратит сбор мусора, но такой вещи также не существует.


FYI, вот еще один актуальный вопрос Почему парадигма деструктора объекта в сборках мусора постоянно пропадает?. В этом обсуждении рассматриваются шаблоны разработки финализатора, деструктора и удаления. Мне было полезно увидеть различие между тремя.

Ответ 3

Вам нужно вручную "уничтожить" объекты в JS. Создание функции уничтожения распространено в JS. На других языках это можно назвать бесплатным, освобождать, распоряжаться, закрывать и т.д. В моем опыте, хотя он имеет тенденцию быть уничтоженным, который отцепляет внутренние ссылки, события и, возможно, распространяет, также уничтожает вызовы на дочерние объекты.

WeakMaps в значительной степени бесполезны, поскольку они не могут быть повторены, и это, вероятно, не будет доступно до ECMA 7, если вообще. Все WeakMaps позволяют вам иметь невидимые свойства, отделенные от самого объекта, кроме поиска по ссылке объекта и GC, чтобы они не мешали ему. Это может быть полезно для кеширования, расширения и использования множественности, но это не помогает в управлении памятью для наблюдаемых и наблюдателей. WeakSet - это подмножество WeakMap (например, WeakMap со значением по умолчанию для логического значения true).

Существуют различные аргументы в отношении того, следует ли использовать различные реализации слабых ссылок для этого или деструкторов. Оба имеют потенциальные проблемы, а деструкторы более ограничены.

Деструкторы фактически бесполезны для наблюдателей/слушателей, потому что обычно слушатель будет прямо или косвенно ссылаться на наблюдателя. Деструктор действительно работает только в прокси-модуле без слабых ссылок. Если ваш Наблюдатель на самом деле просто прокси-сервер, который принимает что-то еще слушателей и помещает их в наблюдаемые, то он может что-то сделать, но такие вещи редко бывают полезны. Деструкторы больше связаны с событиями, связанными с IO, или делают что-то вне области сдерживания (IE, связывая два экземпляра, которые он создал).

Конкретный случай, который я начал изучать, это потому, что у меня есть экземпляр класса A, который принимает класс B в конструкторе, а затем создает экземпляр класса C, который слушает B. Я всегда держу экземпляр B где-то выше. AI иногда выбрасывает, создает новые, создает множество и т.д. В этой ситуации деструктор действительно работает для меня, но с неприятным побочным эффектом, который у родителя, если я передал экземпляр C вокруг, но удалил все ссылки A, тогда C и Связывание B будет сломан (C удаляет землю из-под нее).

В JS, не имеющем автоматического решения, больно, но я не думаю, что это легко разрешимо. Рассмотрим эти классы (псевдо):

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

С другой стороны, трудно сделать работу без анонимных/уникальных функций, которые будут рассмотрены позже.

В обычном случае экземпляр будет таким же (псевдо):

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

В GC эти обычно вы должны установить их в null, но это не будет работать, потому что они создали дерево с stdin в корне. Это в основном то, что делают системы событий. Вы даете родительский родитель ребенку, ребенок добавляет себя к родительскому лицу, а затем может или не может содержать ссылку на родителя. Дерево - простой пример, но на самом деле вы также можете оказаться с сложными графами, хотя и редко.

В этом случае фильтр добавляет ссылку на себя на stdin в виде анонимной функции, которая косвенно ссылается на Filter by scope. Ссылочные ссылки - это то, о чем нужно знать, и это может быть довольно сложным. Мощный GC может сделать некоторые интересные вещи, чтобы вырезать предметы в переменных области, но это другая тема. Важно понимать, что когда вы создаете анонимную функцию и добавляете ее к чему-то в качестве слушателя для ab наблюдаемого, наблюдаемый будет поддерживать ссылку на функцию и что-либо, что функция ссылается в областях, находящихся над ней (что она была определена в ) также будет сохранена. Представления делают то же самое, но после выполнения их конструкторов дети не поддерживают ссылку на своих родителей.

Если я установил все или все объявленные выше vars равными нулю, это не будет иметь никакого отношения ни к чему (аналогично, когда он закончит эту "основную" область). Они по-прежнему будут активны и передают данные из stdin в stdout и stderr.

Если я установил их все в null, было бы невозможно удалить их или скомпилировать без очистки событий на stdin или установки stdin в null (при условии, что он может быть освобожден так). У вас в основном есть утечка памяти таким образом, что в результате осиротевшие объекты, если остальная часть кода нуждается в stdin и имеет другие важные события, запрещающие вам выполнять вышеупомянутые.

Чтобы избавиться от df, v1 и v2, мне нужно вызвать метод destroy для каждого из них. С точки зрения реализации это означает, что и методы "Фильтр", и "Просмотр" должны содержать ссылку на функцию анонимного прослушивателя, которую они создают, а также наблюдаемые и передавать это для удаленияListener.

На стороне примечания, в качестве альтернативы, вы можете иметь наблюдаемый, который возвращает индекс, чтобы отслеживать слушателей, чтобы вы могли добавлять прототипированные функции, которые, по крайней мере, для моего понимания должны быть намного лучше по производительности и памяти. Вы все равно должны отслеживать возвращаемый идентификатор и передавать свой объект, чтобы убедиться, что слушатель связан с ним при вызове.

Функция уничтожения добавляет несколько болей. Во-первых, я должен был бы назвать это и освободить ссылку:

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

Это незначительное раздражение, так как это немного больше кода, но это не настоящая проблема. Когда я передаю эти ссылки ко многим объектам. В этом случае, когда именно вы называете уничтожение? Вы не можете просто передать их другим объектам. Вы получите цепочки разрушений и ручную реализацию отслеживания либо через поток программы, либо какие-то другие средства. Вы не можете стрелять и забыть.

Примером такого типа проблемы является то, что я решил, что View также вызовет destroy на df, когда он будет уничтожен. Если v2 все еще разрушает df, он сломает его, поэтому уничтожить нельзя просто передать в df. Вместо этого, когда v1 принимает df, чтобы использовать его, ему нужно будет сказать, что он используется, чтобы поднять некоторый счетчик или аналогично df. Функция df destroy будет уменьшаться, чем счетчик, и только на самом деле уничтожить, если она равна 0. Такая вещь добавляет много сложности и добавляет много, что может пойти не так, наиболее очевидное из которых разрушает что-то, пока есть где-то ссылка будут использоваться и циклические ссылки (на данный момент это уже не случай управления счетчиком, а карта объектов ссылок). Когда вы думаете о внедрении собственных счетчиков ссылок, ММ и т.д. В JS, то это, вероятно, недостаточно.

Если WeakSets были итерируемыми, это можно было бы использовать:

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

В этом случае собственный класс должен также содержать ссылку на токен, иначе он будет пуп.

Если Observable использовался вместо EventListener, тогда управление памятью было бы автоматически в отношении прослушивателей событий.

Вместо вызова destroy на каждом объекте этого будет достаточно, чтобы полностью удалить их:

df = v1 = v2 = null;

Если вы не установили df в null, он все равно будет существовать, но v1 и v2 будут автоматически отцеплены.

Однако с этим подходом существуют две проблемы.

Проблема заключается в том, что она добавляет новую сложность. Иногда люди действительно не хотят этого поведения. Я мог бы создать очень большую цепочку объектов, связанных друг с другом событиями, а не сдерживанием (ссылки в объектах конструктора или свойствах объекта). В конечном итоге дерево и я должны были бы только обходить корень и беспокоиться об этом. Освобождение корня удобно освобождает все. Оба поведения в зависимости от стиля кодирования и т.д. Полезны, и при создании объектов многократного использования трудно будет знать, чего хотят люди, что они сделали, что вы сделали и боль, чтобы обойти то, что было сделано. Если я использую Observable вместо EventListener, то либо df нужно будет ссылаться на v1 и v2, либо мне придется передать их все, если я хочу передать право собственности на ссылку на что-то еще из области видимости. Слабая ссылка, подобная этой вещи, немного облегчила бы проблему, передав контроль от наблюдаемого наблюдателю, но не решит его полностью (и нуждается в проверке на каждом испускании или событии сам по себе). Эта проблема может быть исправлена. Я полагаю, что если поведение применимо только к изолированным графам, что серьезно осложнит GC и не будет применяться к случаям, когда есть ссылки за пределами графика, которые на практике являются noops (только потребляют циклы процессора, никаких изменений не сделано).

Проблема состоит в том, что либо она непредсказуема в некоторых случаях, либо заставляет JS-движок пересекать график GC для тех объектов по требованию, которые могут иметь ужасающее влияние на производительность (хотя, если он умный, он может избежать выполнения этого элемента на делая это за один цикл WeakMap). GC никогда не будет работать, если использование памяти не достигнет определенного порога, и объект со своими событиями не будет удален. Если я установил v1 в значение null, он все равно может передавать на stdout навсегда. Даже если он получит GCed, это будет произвольным, он может продолжать ретранслировать на stdout на любое количество времени (1 строка, 10 строк, 2,5 строки и т.д.).

Причина, по которой WeakMap уходит, не заботясь о GC, когда non-iterable является тем, что для доступа к объекту у вас есть ссылка на него в любом случае, либо он не был GCed или не был добавлен на карту.

Я не уверен, что я думаю об этом. Вы как бы разрываете управление памятью, чтобы исправить ее с помощью итеративного подхода WeakMap. Вторая проблема может существовать и для деструкторов.

Все это вызывает несколько уровней ада, поэтому я предлагаю попытаться обойти его с хорошим дизайном программы, хорошей практикой, избеганием определенных вещей и т.д. Однако это может расстраивать JS из-за того, насколько гибко это происходит в определенных а также потому, что он более естественно асинхронен и основан на событиях с тяжелой инверсией управления.

Есть еще одно довольно элегантное решение, но у него все еще есть потенциально серьезные зависания. Если у вас есть класс, который расширяет наблюдаемый класс, вы можете переопределить функции события. Добавьте свои события в другие наблюдаемые только тогда, когда события добавляются к себе. Когда все события удаляются из вас, удалите события из детей. Вы также можете сделать класс, чтобы расширить свой наблюдаемый класс, чтобы сделать это за вас. Такой класс может обеспечить крючки для пустых и непустых, так что, поскольку вы будете наблюдать за собой. Этот подход не плох, но также имеет зависания. Существует увеличение сложности, а также снижение производительности. Вам нужно будет сохранить ссылку на объект, который вы наблюдаете. Критически это также не будет работать для листьев, но, по крайней мере, промежуточные продукты будут самоуничтожаться, если вы уничтожаете лист. Это похоже на цепочку уничтожения, но скрытую за звонками, которые вам уже нужно цеплять. Большая проблема с производительностью заключается в том, что вам, возможно, придется повторно инициализировать внутренние данные из Observable каждый раз, когда ваш класс становится активным. Если этот процесс занимает очень много времени, у вас могут быть проблемы.

Если вы могли бы итерации WeakMap, тогда вы могли бы объединить вещи (переключитесь на "Слабый", если нет событий, Сильные, когда события), но все, что действительно делает, - это проблема производительности на ком-то еще.

Есть также непосредственные неприятности с итерируемой WeakMap, когда дело касается поведения. Я кратко упомянул о функциях, имеющих ссылки на сферу применения и резьбу. Если я создаю экземпляр дочернего элемента в конструкторе, который перехватывает прослушиватель "console.log(param)" родительскому элементу и не удается сохранить родительский объект, тогда, когда я удаляю все ссылки на дочерний элемент, он может быть полностью освобожден, поскольку анонимная функция добавлена ​​в родитель ничего не связывает с ребенком. Это оставляет вопрос о том, что делать с parent.weakmap.add(child, (param) = > console.log(param)). Насколько мне известно, ключ слабый, но не значение, так что weakmap.add(объект, объект) является постоянным. Это то, что мне нужно переоценить. Для меня это выглядит как утечка памяти, если я распоряжусь всеми другими объектными ссылками, но я подозреваю, что в действительности это управляет тем, что в основном, рассматривая это как круговую ссылку. Любая анонимная функция поддерживает неявную ссылку на объекты, полученные из родительских областей, для согласованности, тратящей много памяти, или у вас есть поведение, зависящее от обстоятельств, которые трудно предсказать или управлять. Я думаю, что первое на самом деле невозможно. В последнем случае, если у меня есть метод класса, который просто берет объект и добавляет console.log, он будет освобожден, когда я очищу ссылки на класс, даже если я вернул функцию и сохранил ссылку. Справедливости ради следует отметить, что этот конкретный сценарий редко требуется законно, но в конечном итоге кто-то найдет угол и будет запрашивать HalfWeakMap, который является итерируемым (бесплатно по выпуску ключей и значений), но это также непредсказуемо (obj = null magical end IO, f = null магически заканчивается IO, оба выполняются на невероятных расстояниях).

Ответ 4

"Деструктор даже не помог бы вам здесь. Если сами слушатели событий все еще ссылаются на ваш объект, то он не сможет собирать мусор до того, как он будет незарегистрирован".

Не так. Цель деструктора - разрешить элементу, который зарегистрировал слушателей, отменить их регистрацию. Как только у объекта не будет других ссылок на него, он будет собирать мусор.

Например, в AngularJS, когда контроллер уничтожается, он может прослушивать событие уничтожения и отвечать на него. Это не то же самое, что автоматически вызывать деструктор, но он закрывается и дает нам возможность удалить прослушиватели, которые были установлены при инициализации контроллера.

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


Ответ 5

Если такого механизма нет, каков шаблон/соглашение для таких проблем?

Термин "очистка" может быть более подходящим, но будет использовать "деструктор", чтобы соответствовать OP

Предположим, что вы пишете какой-то javascript полностью с 'function and' var's. Затем вы можете использовать шаблон написания всего кода function в рамках решетки try/catch/finally. Внутри finally выполнить код уничтожения.

Вместо стиля C++ написания классов объектов с неопределенными временами жизни, а затем указания времени жизни произвольными областями и неявного вызова ~() в конце области (~() является деструктором в C++), в этом шаблоне javascript объект - это функция, область действия - это область действия функции, а деструктор - это блок finally.

Если вы сейчас думаете, что этот шаблон по своей сути ошибочен, потому что try/catch/finally не включает асинхронное выполнение, которое необходимо для javascript, тогда вы правы. К счастью, с 2018 года объект Promise асинхронного программирования имеет функцию-прототип, finally добавленную к уже существующим функциям resolve и catch прототипа. Это означает, что асинхронные области действия, требующие деструкторов, можно записать с помощью объекта Promise, используя, finally деструктор. Кроме того, вы можете использовать try/catch/finally в async function вызывающей Promise с или без await, но следует помнить, что Promise вызываемый без await, будет выполняться асинхронно за пределами области действия, и поэтому обрабатывать код десктруктора в финале then.

В следующем коде PromiseA и PromiseB - некоторые устаревшие обещания уровня API, в которых finally определены аргументы функции. PromiseC имеет окончательный аргумент.

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 */ }
}

Я не защищаю, чтобы каждый объект в javascript был написан как функция. Вместо этого рассмотрим случай, когда у вас есть определенная область, которая действительно "хочет" вызвать деструктор в конце его жизни. Сформулируйте эту область видимости как функциональный объект, используя в качестве деструктора блок finally (или функцию finally в случае асинхронной области). Вполне вероятно, что формулировка этого функционального объекта устраняет необходимость в нефункциональном классе, который в противном случае был бы написан - никакого дополнительного кода не требовалось, выравнивание области видимости и класса могло бы быть даже чище.

Примечание: как писали другие, мы не должны путать деструкторы и сборщик мусора. Как это бывает, деструкторы C++ часто или в основном занимаются ручным сбором мусора, но не исключительно. В Javascript не требуется ручная сборка мусора, но конец срока службы асинхронной области часто является местом (для) регистрации прослушивателей событий и т.д.