简体   繁体   English

ECMAScript 6 类析构函数

[英]ECMAScript 6 class destructor

I know ECMAScript 6 has constructors but is there such a thing as destructors for ECMAScript 6?我知道 ECMAScript 6 有构造函数,但是 ECMAScript 6 有析构函数吗?

For example if I register some of my object's methods as event listeners in the constructor, I want to remove them when my object is deleted.例如,如果我在构造函数中将对象的某些方法注册为事件侦听器,我想在对象被删除时将它们删除。

One solution is to have a convention of creating a destructor method for every class that needs this kind of behaviour and manually call it.一种解决方案是约定为每个需要这种行为的类创建一个destructor方法并手动调用它。 This will remove the references to the event handlers, hence my object will truly be ready for garbage collection.这将删除对事件处理程序的引用,因此我的对象将真正准备好进行垃圾收集。 Otherwise it'll stay in memory because of those methods.否则,由于这些方法,它会留在内存中。

But I was hoping if ECMAScript 6 has something native that will be called right before the object is garbage collected.但我希望 ECMAScript 6 有一些本机的东西,可以在对象被垃圾收集之前调用。

If there is no such mechanism, what is a pattern/convention for such problems?如果没有这样的机制,这些问题的模式/约定是什么?

Is there such a thing as destructors for ECMAScript 6? ECMAScript 6 有析构函数这样的东西吗?

No. EcmaScript 6 does not specify any garbage collection semantics at all [1] , so there is nothing like a "destruction" either.不,EcmaScript 6 根本没有指定任何垃圾收集语义[1] ,因此也没有像“破坏”这样的东西。

If I register some of my object's methods as event listeners in the constructor, I want to remove them when my object is deleted如果我在构造函数中注册我的一些对象的方法作为事件监听器,我想在我的对象被删除时删除它们

A destructor wouldn't even help you here.析构函数甚至不会在这里帮助您。 It's the event listeners themselves that still reference your object, so it would not be able to get garbage-collected before they are unregistered.事件侦听器本身仍然引用您的对象,因此在取消注册之前将无法进行垃圾收集。
What you are actually looking for is a method of registering listeners without marking them as live root objects.您实际上正在寻找的是一种注册侦听器而不将它们标记为活动根对象的方法。 (Ask your local eventsource manufacturer for such a feature). (请向您当地的事件源制造商咨询此类功能)。

1): Well, there is a beginning with the specification of WeakMap and WeakSet objects. 1):嗯,有一个开始与WeakMapWeakSet对象的规范。 However, true weak references are still in the pipeline[1] [2] .然而,真正的弱引用仍在管道中[1] [2]

I just came across this question in a search about destructors and I thought there was an unanswered part of your question in your comments, so I thought I would address that.我刚刚在搜索析构函数时遇到了这个问题,我认为你的评论中有一个未回答的问题,所以我想我会解决这个问题。

thank you guys.感谢你们。 But what would be a good convention if ECMAScript doesn't have destructors?但是,如果 ECMAScript 没有析构函数,那么好的约定是什么? Should I create a method called destructor and call it manually when I'm done with the object?我应该创建一个名为 destructor 的方法并在完成对象后手动调用它吗? Any other idea?还有什么想法吗?

If you want to tell your object that you are now done with it and it should specifically release any event listeners it has, then you can just create an ordinary method for doing that.如果你想告诉你的对象你现在已经完成了它并且它应该专门释放它拥有的任何事件监听器,那么你可以创建一个普通的方法来做到这一点。 You can call the method something like release() or deregister() or unhook() or anything of that ilk.您可以调用该方法,例如release()deregister()unhook()或类似的任何东西。 The idea is that you're telling the object to disconnect itself from anything else it is hooked up to (deregister event listeners, clear external object references, etc...).这个想法是你告诉对象将自己与它所连接的任何其他东西断开连接(取消注册事件侦听器,清除外部对象引用等......)。 You will have to call it manually at the appropriate time.您必须在适当的时候手动调用它。

If, at the same time you also make sure there are no other references to that object, then your object will become eligible for garbage collection at that point.如果同时您还确保没有其他对该对象的引用,那么您的对象届时将有资格进行垃圾回收。

ES6 does have weakMap and weakSet which are ways of keeping track of a set of objects that are still alive without affecting when they can be garbage collected, but it does not provide any sort of notification when they are garbage collected. ES6 确实有 weakMap 和 weakSet ,它们是跟踪一组仍然活着的对象而不影响它们何时可以被垃圾收集的方法,但是当它们被垃圾收集时它不提供任何类型的通知。 They just disappear from the weakMap or weakSet at some point (when they are GCed).它们只是在某个时候从weakMap 或weakSet 中消失(当它们被GC 时)。


FYI, the issue with this type of destructor you ask for (and probably why there isn't much of a call for it) is that because of garbage collection, an item is not eligible for garbage collection when it has an open event handler against a live object so even if there was such a destructor, it would never get called in your circumstance until you actually removed the event listeners.仅供参考,您要求的这种类型的析构函数的问题(可能是为什么没有太多调用它)是因为垃圾收集,当一个项目有一个打开的事件处理程序时,它不符合垃圾收集的条件一个活动对象,因此即使有这样的析构函数,在您实际删除事件侦听器之前,它也永远不会在您的情况下被调用。 And, once you've removed the event listeners, there's no need for the destructor for this purpose.而且,一旦您删除了事件侦听器,就不需要为此目的使用析构函数。

I suppose there's a possible weakListener() that would not prevent garbage collection, but such a thing does not exist either.我想有一个可能的weakListener()不会阻止垃圾收集,但这样的事情也不存在。


FYI, here's another relevant question Why is the object destructor paradigm in garbage collected languages pervasively absent?仅供参考,这是另一个相关问题为什么垃圾收集语言中的对象析构函数范式普遍存在? . . This discussion covers finalizer, destructor and disposer design patterns.本讨论涵盖终结器、析构器和处置器设计模式。 I found it useful to see the distinction between the three.我发现看到这三者之间的区别很有用。


Edit in 2020 - proposal for object finalizer 2020 年编辑 - 对象终结器提案

There is a Stage 3 EMCAScript proposal to add a user-defined finalizer function after an object is garbage collected.有一个阶段 3 EMCAScript 提议在对象被垃圾回收后添加一个用户定义的终结器函数。

A canonical example of something that would benefit from a feature like this is an object that contains a handle to an open file.可以从此类功能中受益的典型示例是包含打开文件句柄的对象。 If the object is garbage collected (because no other code still has a reference to it), then this finalizer scheme allows one to at least put a message to the console that an external resource has just been leaked and code elsewhere should be fixed to prevent this leak.如果对象被垃圾回收(因为没有其他代码仍然引用它),那么这个终结器方案允许至少向控制台发送一条消息,表明外部资源刚刚被泄露,并且其他地方的代码应该被修复以防止这个泄漏。

If you read the proposal thoroughly, you will see that it's nothing like a full-blown destructor in a language like C++.如果您仔细阅读该提案,您会发现它与 C++ 等语言中的成熟析构函数完全不同。 This finalizer is called after the object has already been destroyed and you have to predetermine what part of the instance data needs to be passed to the finalizer for it to do its work.这个终结器在对象已经被销毁之后被调用,你必须预先确定需要将实例数据的哪一部分传递给终结器才能完成它的工作。 Further, this feature is not meant to be relied upon for normal operation, but rather as a debugging aid and as a backstop against certain types of bugs.此外,此功能并不意味着依赖于正常操作,而是作为调试辅助和针对某些类型的错误的支持。 You can read the full explanation for these limitations in the proposal.您可以阅读提案中这些限制的完整说明。

You have to manually "destruct" objects in JS.您必须在 JS 中手动“破坏”对象。 Creating a destroy function is common in JS.创建销毁函数在 JS 中很常见。 In other languages this might be called free, release, dispose, close, etc. In my experience though it tends to be destroy which will unhook internal references, events and possibly propagates destroy calls to child objects as well.在其他语言中,这可能被称为 free、release、dispose、close 等。根据我的经验,虽然它往往是 destroy ,这将解开内部引用、事件并可能传播对子对象的销毁调用。

WeakMaps are largely useless as they cannot be iterated and this probably wont be available until ECMA 7 if at all. WeakMaps 在很大程度上是无用的,因为它们不能被迭代,而且这可能要到 ECMA 7 才能使用。 All WeakMaps let you do is have invisible properties detached from the object itself except for lookup by the object reference and GC so that they don't disturb it. WeakMaps 让你做的所有事情都是从对象本身分离不可见的属性,除了通过对象引用和 GC 进行查找,这样它们就不会干扰它。 This can be useful for caching, extending and dealing with plurality but it doesn't really help with memory management for observables and observers.这对于缓存、扩展和处理复数很有用,但它对可观察对象和观察者的内存管理并没有真正的帮助。 WeakSet is a subset of WeakMap (like a WeakMap with a default value of boolean true). WeakSet 是 WeakMap 的子集(类似于默认值为 boolean true 的 WeakMap)。

There are various arguments on whether to use various implementations of weak references for this or destructors.关于是否为 this 或析构函数使用各种弱引用实现存在各种争论。 Both have potential problems and destructors are more limited.两者都有潜在的问题,并且析构函数更加有限。

Destructors are actually potentially useless for observers/listeners as well because typically the listener will hold references to the observer either directly or indirectly.析构函数实际上对观察者/侦听器也可能无用,因为通常侦听器将直接或间接持有对观察者的引用。 A destructor only really works in a proxy fashion without weak references.析构函数仅在没有弱引用的情况下真正以代理方式工作。 If your Observer is really just a proxy taking something else's Listeners and putting them on an observable then it can do something there but this sort of thing is rarely useful.如果您的 Observer 真的只是一个代理,接收其他东西的 Listeners 并将它们放在一个 observable 上,那么它可以在那里做一些事情,但这种事情很少有用。 Destructors are more for IO related things or doing things outside of the scope of containment (IE, linking up two instances that it created).析构函数更多地用于 IO 相关的事情或在包含范围之外的事情(IE,链接它创建的两个实例)。

The specific case that I started looking into this for is because I have class A instance that takes class B in the constructor, then creates class C instance which listens to B. I always keep the B instance around somewhere high above.我开始研究这个的具体情况是因为我有一个类 A 实例,它在构造函数中接受类 B,然后创建监听 B 的类 C 实例。我总是将 B 实例保持在高处。 AI sometimes throw away, create new ones, create many, etc. In this situation a Destructor would actually work for me but with a nasty side effect that in the parent if I passed the C instance around but removed all A references then the C and B binding would be broken (C has the ground removed from beneath it). AI 有时会丢弃、创建新的、创建许多等等。在这种情况下,析构函数实际上对我有用,但如果我传递 C 实例但删除所有 A 引用然后删除 C 和B 绑定将被破坏(C 已从其下方移除地面)。

In JS having no automatic solution is painful but I don't think it's easily solvable.在 JS 中没有自动解决方案是痛苦的,但我认为它不容易解决。 Consider these classes (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.
    });
}

On a side note, it's hard to make things work without anonymous/unique functions which will be covered later.附带说明一下,如果没有稍后将介绍的匿名/唯一函数,就很难使事情正常进行。

In a normal case instantiation would be as so (pseudo):在正常情况下,实例化会是这样(伪):

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

To GC these normally you would set them to null but it wont work because they've created a tree with stdin at the root.要 GC 这些通常你会将它们设置为 null 但它不会工作,因为它们已经创建了一个以标准输入为根的树。 This is basically what event systems do.这基本上就是事件系统所做的。 You give a parent to a child, the child adds itself to the parent and then may or may not maintain a reference to the parent.你给一个孩子一个父母,孩子将自己添加到父母中,然后可能会或可能不会维护对父母的引用。 A tree is a simple example but in reality you may also find yourself with complex graphs albeit rarely.树是一个简单的例子,但实际上你也可能会发现自己有复杂的图表,尽管很少。

In this case, Filter adds a reference to itself to stdin in the form of an anonymous function which indirectly references Filter by scope.在这种情况下,Filter 以匿名函数的形式向标准输入添加对自身的引用,该函数通过范围间接引用 Filter。 Scope references are something to be aware of and that can be quite complex.范围引用是需要注意的,并且可能非常复杂。 A powerful GC can do some interesting things to carve away at items in scope variables but that's another topic.强大的 GC 可以做一些有趣的事情来分割范围变量中的项目,但这是另一个主题。 What is critical to understand is that when you create an anonymous function and add it to something as a listener to ab observable, the observable will maintain a reference to the function and anything the function references in the scopes above it (that it was defined in) will also be maintained.理解的关键是,当您创建一个匿名函数并将其作为监听器添加到某事物中时,可观察对象将维护对该函数的引用以及该函数在其上方范围内引用的任何内容(它在) 也将被保留。 The views do the same but after the execution of their constructors the children do not maintain a reference to their parents.视图执行相同的操作,但在执行其构造函数后,子级不会维护对其父级的引用。

If I set any or all of the vars declared above to null it isn't going to make a difference to anything (similarly when it finished that "main" scope).如果我将上面声明的任何或所有变量设置为 null 它不会对任何事情产生影响(类似地,当它完成那个“主”范围时)。 They will still be active and pipe data from stdin to stdout and stderr.它们仍将处于活动状态,并将数据从标准输入传送到标准输出和标准错误。

If I set them all to null it would be impossible to have them removed or GCed without clearing out the events on stdin or setting stdin to null (assuming it can be freed like this).如果我将它们全部设置为 null,那么如果不清除 stdin 上的事件或将 stdin 设置为 null(假设可以像这样释放它),就不可能将它们删除或 GC。 You basically have a memory leak that way with in effect orphaned objects if the rest of the code needs stdin and has other important events on it prohibiting you from doing the aforementioned.如果其余代码需要标准输入并且有其他重要事件禁止您执行上述操作,那么您基本上会以这种方式与有效的孤立对象发生内存泄漏。

To get rid of df, v1 and v2 I need to call a destroy method on each of them.为了摆脱 df、v1 和 v2,我需要对它们中的每一个调用一个 destroy 方法。 In terms of implementation this means that both the Filter and View methods need to keep the reference to the anonymous listener function they create as well as the observable and pass that to removeListener.在实现方面,这意味着 Filter 和 View 方法都需要保留对它们创建的匿名侦听器函数的引用以及 observable 并将其传递给 removeListener。

On a side note, alternatively you can have an obserable that returns an index to keep track of listeners so that you can add prototyped functions which at least to my understanding should be much better on performance and memory.附带说明一下,或者您可以拥有一个可返回索引以跟踪侦听器的 oberable,以便您可以添加原型函数,至少在我看来,这些函数在性能和内存方面应该会更好。 You still have to keep track of the returned identifier though and pass your object to ensure that the listener is bound to it when called.您仍然必须跟踪返回的标识符并传递您的对象以确保侦听器在调用时绑定到它。

A destroy function adds several pains.销毁函数会增加一些麻烦。 First is that I would have to call it and free the reference:首先是我必须调用它并释放参考:

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

This is a minor annoyance as it's a bit more code but that is not the real problem.这是一个小烦恼,因为它需要更多代码,但这不是真正的问题。 When I hand these references around to many objects.当我将这些引用传递给许多对象时。 In this case when exactly do you call destroy?在这种情况下,您究竟什么时候调用破坏? You cannot simply hand these off to other objects.您不能简单地将这些交给其他对象。 You'll end up with chains of destroys and manual implementation of tracking either through program flow or some other means.您最终将通过程序流或其他方式获得破坏链和手动实现跟踪。 You can't fire and forget.你不能开枪就忘记。

An example of this kind of problem is if I decide that View will also call destroy on df when it is destroyed.此类问题的一个示例是,如果我决定 View 也会在 df 被销毁时调用 destroy 。 If v2 is still around destroying df will break it so destroy cannot simply be relayed to df.如果 v2 仍在销毁 df 将破坏它,因此不能简单地将销毁转发给 df。 Instead when v1 takes df to use it, it would need to then tell df it is used which would raise some counter or similar to df.相反,当 v1 使用 df 使用它时,它需要告诉 df 它已被使用,这将引发一些计数器或类似于 df。 df's destroy function would decrease than counter and only actually destroy if it is 0. This sort of thing adds a lot of complexity and adds a lot that can go wrong the most obvious of which is destroying something while there is still a reference around somewhere that will be used and circular references (at this point it's no longer a case of managing a counter but a map of referencing objects). df 的销毁函数会比计数器减少,并且只有在它为 0 时才真正销毁。这种事情增加了很多复杂性,并增加了很多可能出错的地方,其中最明显的是销毁某些东西,而在某处仍有参考将被使用和循环引用(此时它不再是管理计数器的情况,而是引用对象的映射)。 When you're thinking of implementing your own reference counters, MM and so on in JS then it's probably deficient.当您考虑在 JS 中实现自己的引用计数器、MM 等时,它可能是有缺陷的。

If WeakSets were iterable, this could be used:如果 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);
};

In this case the owning class must also keep a token reference to f otherwise it will go poof.在这种情况下,拥有类还必须保留对 f 的令牌引用,否则它将失败。

If Observable were used instead of EventListener then memory management would be automatic in regards to the event listeners.如果使用 Observable 而不是 EventListener,那么关于事件监听器的内存管理将是自动的。

Instead of calling destroy on each object this would be enough to fully remove them:而不是在每个对象上调用destroy,这足以完全删除它们:

df = v1 = v2 = null;

If you didn't set df to null it would still exist but v1 and v2 would automatically be unhooked.如果您没有将 df 设置为 null 它仍然存在,但 v1 和 v2 将自动取消挂钩。

There are two problems with this approach however.然而,这种方法存在两个问题。

Problem one is that it adds a new complexity.问题之一是它增加了新的复杂性。 Sometimes people do not actually want this behaviour.有时人们实际上并不想要这种行为。 I could create a very large chain of objects linked to each other by events rather than containment (references in constructor scopes or object properties).我可以创建一个非常大的对象链,它们通过事件而不是包含(构造函数范围或对象属性中的引用)相互链接。 Eventually a tree and I would only have to pass around the root and worry about that.最终一棵树,我只需要绕过根部并担心这一点。 Freeing the root would conveniently free the entire thing.释放根可以方便地释放整个事物。 Both behaviours depending on coding style, etc are useful and when creating reusable objects it's going to be hard to either know what people want, what they have done, what you have done and a pain to work around what has been done.取决于编码风格等的这两种行为都是有用的,当创建可重用的对象时,很难知道人们想要什么,他们做了什么,你做了什么,并且很难解决已经完成的事情。 If I use Observable instead of EventListener then either df will need to reference v1 and v2 or I'll have to pass them all if I want to transfer ownership of the reference to something else out of scope.如果我使用 Observable 而不是 EventListener 则 df 将需要引用 v1 和 v2 或者如果我想将引用的所有权转移给超出范围的其他内容,则必须将它们全部传递。 A weak reference like thing would mitigate the problem a little by transferring control from Observable to an observer but would not solve it entirely (and needs check on every emit or event on itself).弱引用之类的东西可以通过将控制从 Observable 转移到观察者来稍微缓解问题,但不会完全解决它(并且需要检查每个发射或事件本身)。 This problem can be fixed I suppose if the behaviour only applies to isolated graphs which would complicate the GC severely and would not apply to cases where there are references outside the graph that are in practice noops (only consume CPU cycles, no changes made).我想如果该行为仅适用于会使 GC 严重复杂化的孤立图,并且不适用于在图外存在实际上是 noops 的引用的情况(仅消耗 CPU 周期,未进行任何更改),则可以解决此问题。

Problem two is that either it is unpredictable in certain cases or forces the JS engine to traverse the GC graph for those objects on demand which can have a horrific performance impact (although if it is clever it can avoid doing it per member by doing it per WeakMap loop instead).问题二是在某些情况下它是不可预测的,或者迫使 JS 引擎根据需要遍历那些对象的 GC 图,这可能会产生可怕的性能影响(尽管如果它很聪明,它可以避免每个成员都这样做而是 WeakMap 循环)。 The GC may never run if memory usage does not reach a certain threshold and the object with its events wont be removed.如果内存使用量没有达到某个阈值并且对象及其事件不会被删除,则 GC 可能永远不会运行。 If I set v1 to null it may still relay to stdout forever.如果我将 v1 设置为 null,它可能仍会永远中继到标准输出。 Even if it does get GCed this will be arbitrary, it may continue to relay to stdout for any amount of time (1 lines, 10 lines, 2.5 lines, etc).即使它确实得到了 GC,这也是任意的,它可能会继续中继到标准输出任意时间(1 行、10 行、2.5 行等)。

The reason WeakMap gets away with not caring about the GC when non-iterable is that to access an object you have to have a reference to it anyway so either it hasn't been GCed or hasn't been added to the map. WeakMap 在不可迭代时不关心 GC 的原因是,要访问一个对象,无论如何你都必须引用它,因此要么它没有被 GC,要么没有被添加到地图中。

I am not sure what I think about this kind of thing.我不确定我对这种事情的看法。 You're sort of breaking memory management to fix it with the iterable WeakMap approach.您有点破坏内存管理以使用可迭代的 WeakMap 方法修复它。 Problem two can also exist for destructors as well.析构函数也可能存在问题二。

All of this invokes several levels of hell so I would suggest to try to work around it with good program design, good practices, avoiding certain things, etc. It can be frustrating in JS however because of how flexible it is in certain aspects and because it is more naturally asynchronous and event based with heavy inversion of control.所有这一切都引发了几个层次的地狱,所以我建议尝试通过良好的程序设计、良好的实践、避免某些事情等来解决它。但是在 JS 中可能会令人沮丧,因为它在某些方面非常灵活,并且因为它更自然地是异步的和基于事件的,具有大量的控制反转。

There is one other solution that is fairly elegant but again still has some potentially serious hangups.还有另一种相当优雅的解决方案,但仍然存在一些潜在的严重挂断。 If you have a class that extends an observable class you can override the event functions.如果您有一个扩展了可观察类的类,则可以覆盖事件函数。 Add your events to other observables only when events are added to yourself.仅当将事件添加到您自己时,才将您的事件添加到其他可观察对象。 When all events are removed from you then remove your events from children.当所有事件都从您身上移除后,再从孩子身上移除您的事件。 You can also make a class to extend your observable class to do this for you.您还可以创建一个类来扩展您的可观察类来为您执行此操作。 Such a class could provide hooks for empty and non-empty so in a since you would be Observing yourself.这样的类可以为空和非空提供挂钩,因此您将在观察自己。 This approach isn't bad but also has hangups.这种方法还不错,但也有挂断。 There is a complexity increase as well as performance decrease.复杂性增加,性能下降。 You'll have to keep a reference to object you observe.您必须保留对您观察到的对象的引用。 Critically, it also will not work for leaves but at least the intermediates will self destruct if you destroy the leaf.至关重要的是,它也不适用于叶子,但如果你破坏叶子,至少中间体会自毁。 It's like chaining destroy but hidden behind calls that you already have to chain.这就像链接破坏但隐藏在您已经必须链接的调用后面。 A large performance problem is with this however is that you may have to reinitialise internal data from the Observable everytime your class becomes active.然而,一个很大的性能问题是,每次你的类变得活跃时,你可能必须从 Observable 重新初始化内部数据。 If this process takes a very long time then you might be in trouble.如果此过程需要很长时间,那么您可能会遇到麻烦。

If you could iterate WeakMap then you could perhaps combine things (switch to Weak when no events, Strong when events) but all that is really doing is putting the performance problem on someone else.如果您可以迭代 WeakMap,那么您也许可以组合事物(在没有事件时切换到弱,在事件时切换到强),但真正要做的就是将性能问题放在其他人身上。

There are also immediate annoyances with iterable WeakMap when it comes to behaviour.当涉及到行为时,可迭代的 WeakMap 也有直接的烦恼。 I mentioned briefly before about functions having scope references and carving.我之前简要提到过具有范围引用和雕刻的函数。 If I instantiate a child that in the constructor that hooks the listener 'console.log(param)' to parent and fails to persist the parent then when I remove all references to the child it could be freed entirely as the anonymous function added to the parent references nothing from within the child.如果我在将侦听器“console.log(param)”挂钩到父级的构造函数中实例化一个子级并且无法持久化父级,那么当我删除对子级的所有引用时,它可以完全释放,因为添加到parent 没有从孩子内部引用任何内容。 This leaves the question of what to do about parent.weakmap.add(child, (param) => console.log(param)).这就留下了如何处理 parent.weakmap.add(child, (param) => console.log(param)) 的问题。 To my knowledge the key is weak but not the value so weakmap.add(object, object) is persistent.据我所知,键是弱的,但不是值,所以 weakmap.add(object, object) 是持久的。 This is something I need to reevaluate though.这是我需要重新评估的东西。 To me that looks like a memory leak if I dispose all other object references but I suspect in reality it manages that basically by seeing it as a circular reference.对我来说,如果我处理所有其他对象引用,这看起来像是内存泄漏,但我怀疑实际上它基本上通过将其视为循环引用来管理它。 Either the anonymous function maintains an implicit reference to objects resulting from parent scopes for consistency wasting a lot of memory or you have behaviour varying based on circumstances which is hard to predict or manage.匿名函数要么维护对从父范围产生的对象的隐式引用,以保持一致性,从而浪费大量内存,要么您的行为根据难以预测或管理的情况而变化。 I think the former is actually impossible.我认为前者实际上是不可能的。 In the latter case if I have a method on a class that simply takes an object and adds console.log it would be freed when I clear the references to the class even if I returned the function and maintained a reference.在后一种情况下,如果我在一个类上有一个方法,它只接受一个对象并添加 console.log,即使我返回了该函数并维护了一个引用,当我清除对该类的引用时,它也会被释放。 To be fair this particular scenario is rarely needed legitimately but eventually someone will find an angle and will be asking for a HalfWeakMap which is iterable (free on key and value refs released) but that is unpredictable as well (obj = null magically ending IO, f = null magically ending IO, both doable at incredible distances).公平地说,这种特殊场景很少合法地需要,但最终有人会找到一个角度并要求一个可迭代的 HalfWeakMap(释放键和值引用时免费)但也是不可预测的(obj = null 神奇地结束 IO, f = null 神奇地结束 IO,两者都可以在难以置信的距离上实现)。

If there is no such mechanism, what is a pattern/convention for such problems?如果没有这样的机制,这些问题的模式/约定是什么?

The term 'cleanup' might be more appropriate, but will use 'destructor' to match OP术语“清理”可能更合适,但将使用“析构函数”来匹配 OP

Suppose you write some javascript entirely with 'function's and 'var's.假设您完全使用 'function's 和 'var's 编写一些 javascript。 Then you can use the pattern of writing all the function s code within the framework of a try / catch / finally lattice.然后你可以使用在try / catch / finally格的框架内编写所有function代码的模式。 Within finally perform the destruction code.finally执行销毁代码。

Instead of the C++ style of writing object classes with unspecified lifetimes, and then specifying the lifetime by arbitrary scopes and the implicit call to ~() at scope end ( ~() is destructor in C++), in this javascript pattern the object is the function, the scope is exactly the function scope, and the destructor is the finally block.而不是 C++ 编写具有未指定生命周期的对象类的风格,然后通过任意范围指定生命周期并在范围结束时对~()的隐式调用( ~()在 C++ 中是析构函数),在这个 JavaScript 模式中,对象是函数,作用域就是函数作用域,析构函数就是finally块。

If you are now thinking this pattern is inherently flawed because try / catch / finally doesn't encompass asynchronous execution which is essential to javascript, then you are correct.如果您现在认为这种模式本质上是有缺陷的,因为try / catch / finally不包含对 javascript 必不可少的异步执行,那么您是正确的。 Fortunately, since 2018 the asynchronous programming helper object Promise has had a prototype function finally added to the already existing resolve and catch prototype functions.幸运的是,自 2018 年以来,异步编程助手对象Promise finally在已经存在的resolvecatch原型函数中添加了一个原型函数。 That means that that asynchronous scopes requiring destructors can be written with a Promise object, using finally as the destructor.这意味着需要析构函数的异步作用域可以用Promise对象编写,使用finally作为析构函数。 Furthermore you can use try / catch / finally in an async function calling Promise s with or without await , but must be aware that Promise s called without await will be execute asynchronously outside the scope and so handle the desctructor code in a final then .此外,您可以在调用Promiseasync function中使用try / catch / finally ,带或不带await ,但必须注意,不带 await 调用的Promise将在范围外异步执行,因此在 final then中处理析构函数代码。

In the following code PromiseA and PromiseB are some legacy API level promises which don't have finally function arguments specified.在下面的代码中, PromiseAPromiseB是一些没有指定finally函数参数的遗留 API 级别的承诺。 PromiseC DOES have a finally argument defined. PromiseC确实定义了一个 finally 参数。

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

I am not advocating that every object in javascript be written as a function.我并不是提倡将 javascript 中的每个对象都写成一个函数。 Instead, consider the case where you have a scope identified which really 'wants' a destructor to be called at its end of life.相反,请考虑这样一种情况:您确定了一个范围,该范围确实“希望”在其生命周期结束时调用析构函数。 Formulate that scope as a function object, using the pattern's finally block (or finally function in the case of an asynchronous scope) as the destructor.将该作用域表述为函数对象,使用模式的finally块(或在异步作用域的情况下为finally函数)作为析构函数。 It is quite like likely that formulating that functional object obviated the need for a non-function class which would otherwise have been written - no extra code was required, aligning scope and class might even be cleaner.很可能制定该功能对象消除了对非功能类的需求,否则该类将被编写 - 不需要额外的代码,对齐范围和类甚至可能更干净。

Note: As others have written, we should not confuse destructors and garbage collection.注意:正如其他人所写,我们不应该混淆析构函数和垃圾收集。 As it happens C++ destructors are often or mainly concerned with manual garbage collection, but not exclusively so.碰巧 C++ 析构函数通常或主要关注手动垃圾收集,但并非完全如此。 Javascript has no need for manual garbage collection, but asynchronous scope end-of-life is often a place for (de)registering event listeners, etc.. Javascript 不需要手动垃圾收集,但异步范围的生命周期结束通常是(取消)注册事件侦听器等的地方。

"A destructor wouldn't even help you here. It's the event listeners themselves that still reference your object, so it would not be able to get garbage-collected before they are unregistered." “在这里,析构函数甚至帮不了你。事件监听器本身仍然在引用你的对象,所以在它们被取消注册之前它无法被垃圾回收。”

Not so.不是这样。 The purpose of a destructor is to allow the item that registered the listeners to unregister them.析构函数的目的是允许注册监听器的项目取消注册它们。 Once an object has no other references to it, it will be garbage collected.一旦一个对象没有其他对它的引用,它将被垃圾收集。

For instance, in AngularJS, when a controller is destroyed, it can listen for a destroy event and respond to it.例如,在 AngularJS 中,当一个控制器被销毁时,它可以监听一个销毁事件并做出响应。 This isn't the same as having a destructor automatically called, but it's close, and gives us the opportunity to remove listeners that were set when the controller was initialized.这与自动调用析构函数不同,但它很接近,让我们有机会删除在控制器初始化时设置的侦听器。

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


Here you go.干得好。 The Subscribe/Publish object will unsubscribe a callback function automatically if it goes out of scope and gets garbage collected.如果订阅/发布对象超出范围并被垃圾收集, Subscribe/Publish对象将自动unsubscribe回调函数。

const createWeakPublisher = () => {
  const weakSet = new WeakSet();
  const subscriptions = new Set();

  return {
    subscribe(callback) {
      if (!weakSet.has(callback)) {
        weakSet.add(callback);
        subscriptions.add(new WeakRef(callback));
      }

      return callback;
    },

    publish() {
      for (const weakRef of subscriptions) {
        const callback = weakRef.deref();
        console.log(callback?.toString());

        if (callback) callback();
        else subscriptions.delete(weakRef);
      }
    },
  };
};

Although it might not happen immediately after the callback function goes out of scope, or it might not happen at all.虽然它可能不会在回调函数超出范围后立即发生,或者根本不会发生。 See weakRef documentation for more details.有关更多详细信息,请参阅weakRef文档。 But it works like a charm for my use case.但它对我的用例来说就像一个魅力。

You might also want to check out the FinalizationRegistry API for a different approach.您可能还想查看FinalizationRegistry API 以了解不同的方法。

Javascript does not have destructures the same way C++ does. Javascript 不像 C++ 那样进行解构。 Instead, alternative design patterns should be used to manage resources.相反,应该使用替代设计模式来管理资源。 Here are a couple of examples:这里有几个例子:

You can restrict users to using the instance for the duration of a callback, after which it'll automatically be cleaned up.您可以限制用户在回调期间使用该实例,之后会自动清理该实例。 (This pattern is similar to the beloved "with" statement in Python) (这种模式类似于 Python 中深受喜爱的“with”语句)

connectToDatabase(async db => {
  const resource = await db.doSomeRequest()
  await useResource(resource)
}) // The db connection is closed once the callback ends

When the above example is too restrictive, another alternative is to just create explicit cleanup functions.当上面的例子过于严格时,另一种选择是只创建显式的清理函数。

const db = makeDatabaseConnection()

const resource = await db.doSomeRequest()
updatePageWithResource(resource)

pageChangeEvent.addListener(() => {
  db.destroy()
})

The other answers already explained in detail that there is no destructor.其他答案已经详细解释了没有析构函数。 But your actual goal seems to be event related.但是您的实际目标似乎与事件有关。 You have an object which is connected to some event and you want this connection to go away automatically when the object is garbage collected.您有一个连接到某个事件的对象,并且您希望此连接在对象被垃圾回收时自动消失。 But this won't happen because the event subscription itself references the listener function.但这不会发生,因为事件订阅本身引用了侦听器函数。 Well, UNLESS you use this nifty new WeakRef stuff.好吧,除非你使用这个漂亮的新WeakRef东西。

Here is an example:这是一个例子:

<!DOCTYPE html>
<html>
  <body>
    <button onclick="subscribe()">Subscribe</button>
    <button id="emitter">Emit</button>
    <button onclick="free()">Free</button>
    <script>

    const emitter = document.getElementById("emitter");
    let listener = null;

    function addWeakEventListener(element, event, callback) {
        // Weakrefs only can store objects, so we put the callback into an object
        const weakRef = new WeakRef({ callback });
        const listener = () => {
            const obj = weakRef.deref();
            if (obj == null) {
                console.log("Removing garbage collected event listener");
                element.removeEventListener(event, listener);
            } else {
                obj.callback();
            }
        };
        element.addEventListener(event, listener);
    }

    function subscribe() {
        listener = () => console.log("Event fired!");
        addWeakEventListener(emitter, "click", listener);
        console.log("Listener created and subscribed to emitter");
    }

    function free() {
        listener = null;
        console.log("Reference cleared. Now force garbage collection in dev console or wait some time before clicking Emit again.");
    }

    </script>
  </body>
</html>

( JSFiddle ) ( JSFiddle )

Clicking the Subscribe button creates a new listener function and registers it at the click event of the Emit button.单击订阅按钮会创建一个新的侦听器函数并将其注册到Emit按钮的单击事件中。 So clicking the Emit button after that prints a message to the console.因此,之后单击Emit按钮会向控制台打印一条消息。 Now click the Free button which simply sets the listener variable to null so the garbage collector can remove the listener.现在单击Free按钮,它只是将侦听器变量设置为 null,以便垃圾收集器可以删除侦听器。 Wait some time or force gargabe collection in the developer console and then click the Emit button again.等待一段时间或在开发者控制台中强制收集垃圾,然后再次单击Emit按钮。 The wrapper listener function now sees that the actual listener (wrapped in a WeakRef) is no longer there and then unsubscribes itself from the button.包装侦听器函数现在看到实际侦听器(包装在 WeakRef 中)不再存在,然后从按钮中取消订阅。

WeakRefs are quite powerful but note that there is no guarantee if and when your stuff is garbage collected. WeakRefs 非常强大,但请注意,不能保证您的东西是否以及何时被垃圾收集。

The answer to the question as-stated in the title is FinalizationRegistry , available since Firefox 79 (June 2020), Chrome 84 and derivatives (July 2020), Safari 14.1 (April 2021), and Node 14.6.0 (July 2020)… however, a native JS destructor is probably not the right solution for your use-case .标题中所述问题的答案是FinalizationRegistry ,自 Firefox 79(2020 年 6 月)、Chrome 84 及其衍生版本(2020 年 7 月)、Safari 14.1(2021 年 4 月)和 Node 14.6.0(2020 年 7 月)起可用……但是,本机JS 析构函数可能不是您用例的正确解决方案

function create_eval_worker(f) {
    let src_worker_blob = new Blob([f.toString()], {type: 'application/javascript'});
    let src_worker_url = URL.createObjectURL(src_worker_blob);

    async function g() {
        let w = new Worker(src_worker_url);
        …
    }

    // Run URL.revokeObjectURL(src_worker_url) as a destructor of g
    let registry = new FinalizationRegistry(u => URL.revokeObjectURL(u));
    registry.register(g, src_worker_url);

    return g;
    }
}

Caveat:警告:

Avoid where possible尽可能避免

Correct use of FinalizationRegistry takes careful thought, and it's best avoided if possible.正确使用 FinalizationRegistry 需要仔细考虑,如果可能,最好避免使用。 When, how, and whether garbage collection occurs is down to the implementation of any given JavaScript engine.何时、如何以及是否发生垃圾收集取决于任何给定 JavaScript 引擎的实现。 Any behavior you observe in one engine may be different in another engine, in another version of the same engine, or even in a slightly different situation with the same version of the same engine.您在一个引擎中观察到的任何行为可能在另一个引擎中、在同一引擎的另一个版本中、甚至在同一引擎的相同版本中略有不同的情况下有所不同。

Developers shouldn't rely on cleanup callbacks for essential program logic.开发人员不应依赖清理回调来实现基本程序逻辑。 Cleanup callbacks may be useful for reducing memory usage across the course of a program, but are unlikely to be useful otherwise.清理回调可能有助于减少整个程序过程中的内存使用,但在其他情况下不太可能有用。

A conforming JavaScript implementation, even one that does garbage collection, is not required to call cleanup callbacks.一个符合标准的 JavaScript 实现,即使是垃圾回收,也不需要调用清理回调。 When and whether it does so is entirely down to the implementation of the JavaScript engine.何时以及是否这样做完全取决于 JavaScript 引擎的实现。 When a registered object is reclaimed, any cleanup callbacks for it may be called then, or some time later, or not at all.当一个注册的对象被回收时,它的任何清理回调可能会被调用,或者稍后,或者根本不调用。

–Mozilla Developer Network ——Mozilla 开发者网络

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM