ECMAScript 6类析构函数


76

我知道ECMAScript 6具有构造函数,但是否有ECMAScript 6的析构函数之类的东西?

例如,如果我将一些对象的方法注册为构造函数中的事件侦听器,则我想在删除对象时将其删除。

一种解决方案是约定desctructor为每个需要这种行为的类创建一个方法,然后手动调用它。这将删除对事件处理程序的引用,因此我的对象将真正准备好进行垃圾回收。否则,由于这些方法,它将保留在内存中。

但是我希望ECMAScript 6是否具有本机功能,可以在对象被垃圾回收之前立即调用。

如果没有这种机制,这种问题的模式/惯例是什么?


2
我不会说析构函数将是正确的选择。垃圾收集器不会在您期望的时候收集内存,因此,我认为仍然需要某种手动处理方式。
马蒂亚斯Fidemraizer


3
如果您有事件侦听器,则在对象消失之前无法对其进行GC处理。在这种情况下,没有一种功能会有用。
Slaks

感谢大伙们。但是,如果ECMAScript没有析构函数,那将是一个好的约定吗?destructor完成对象后,我应该创建一个称为并手动调用的方法吗?还有其他想法吗?
AlexStack

将事件处理程序绑定部分分为两种方法:this.subscribe()和this.unsubscribe()
dandavis 2015年

Answers:


34

是否存在ECMAScript 6的析构函数?

否。EcmaScript6根本没有指定任何垃圾回收语义[1],因此也没有像“销毁”那样的东西。

如果我将对象的某些方法注册为构造函数中的事件侦听器,则想在删除对象时将其删除

析构函数甚至都不会在这里为您提供帮助。事件侦听器本身仍在引用您的对象,因此在取消注册之前将无法对其进行垃圾回收。
您实际上正在寻找的是一种注册侦听器而不将其标记为活动根对象的方法。(向您的本地事件源制造商咨询该功能)。

1):好,从WeakMapWeakSet对象的规范开始。但是,真正的弱引用仍然在管道中[1] [2]


13
Dam,期待这些真正的Javascript析构函数。
佩里耶

1
这将非常有帮助。像React中的东西将是objectWillUnmount
Jason Sebring

@nurettin与析构函数无关吗?(顺便说一句,您使用的是ES8 async/ await,而不是ES9 Promise.prototype.finally。)
Bergi

37

我只是在搜索有关析构函数的过程中遇到了这个问题,我认为您的评论中有您问题的未解决部分,所以我想解决一下。

感谢大伙们。但是,如果ECMAScript没有析构函数,那将是一个好的约定吗?我是否应该创建一个称为析构函数的方法,并在处理完对象后手动调用它?还有其他想法吗?

如果您想告诉对象您现在已经完成它,并且它应该专门释放它拥有的所有事件侦听器,那么您只需创建一个普通方法即可。您可以调用该方法,例如release()orderegister()unhook()or之类的东西。这个想法是,您要告诉对象将自身与连接的其他任何对象断开连接(注销事件侦听器,清除外部对象引用等)。您将必须在适当的时间手动调用它。

如果同时还确保没有其他对该对象的引用,那么此时您的对象将有资格进行垃圾回收。

ES6确实具有weakMap和weakSet,这是在不影响何时可以对其进行垃圾收集的情况下跟踪仍处于活动状态的一组对象的方式,但是在对其进行垃圾收集时它不提供任何类型的通知。它们只是在某个时刻(当它们被GC化时)从weakMap或weakSet中消失了。


仅供参考,您要求的这种析构函数的问题(以及为什么没有这么多要求)是因为垃圾回收,当某个项目具有针对一个活动对象,因此即使存在这样的析构函数,在您实际删除事件侦听器之前,在您的情况下也永远不会调用它。而且,一旦删除了事件侦听器,就不需要为此使用析构函数。

我想有一种可能weakListener()不会阻止垃圾回收,但是这样的事情也不存在。


仅供参考,这是另一个相关的问题,为什么垃圾收集语言中的对象析构函数范式普遍缺失?。该讨论涵盖了终结器,析构器和处置器设计模式。我发现查看三者之间的区别很有用。


2020年编辑-对象最终定稿的建议

有一个第3阶段EMCAScript提案添加一个用户定义的功能终结后一个目的是垃圾收集。

受益于此类功能的事物的一个典型示例是包含打开文件句柄的对象。如果该对象是垃圾回收的(因为没有其他代码仍然引用该对象),则此终结器方案允许至少向控制台发送一条消息,即外部资源刚刚泄漏,并且应该修复其他地方的代码以防止这个泄漏。

如果您仔细阅读了该建议,将会发现它与C ++之类的成熟析构函数完全不同。在对象已被销毁后调用此终结器,您必须预先确定实例数据的哪一部分需要传递给终结器以使其完成工作。此外,此功能并不意味着在正常操作时将依赖该功能,而应作为调试辅助和针对某些错误的支持。您可以在提案中阅读有关这些限制的完整说明。


15

您必须在JS中手动“销毁”对象。创建销毁函数在JS中很常见。在其他语言中,这可以称为“释放”,“释放”,“处置”,“关闭”等。以我的经验,尽管它倾向于被销毁,这将取消内部引用,事件并可能将销毁调用传播给子对象。

WeakMaps基本上是无用的,因为它们无法迭代,并且可能根本无法使用,直到ECMA 7。WeakMaps所要做的就是从对象本身分离出不可见的属性,除了通过对象引用和GC查找之外,这样它们就不会干扰它。这对于缓存,扩展和处理复数可能很有用,但对于可观察对象和观察者的内存管理并没有真正帮助。WeakSet是WeakMap的子集(例如默认值为boolean true的WeakMap)。

关于是否为此或析构函数使用弱引用的各种实现,存在各种争论。两者都有潜在的问题,并且破坏者更受限制。

析构函数实际上也可能对观察者/侦听器无用,因为通常侦听器将直接或间接持有对观察者的引用。析构函数仅在没有弱引用的情况下才真正以代理方式工作。如果您的观察者实际上只是一个代理,可以将其他监听器放在另一个可观察者上,那么它可以在那里做点什么,但是这种事情很少有用。析构函数更多地用于与IO相关的事情或超出包含范围的事情(IE,将它创建的两个实例链接起来)。

我开始研究的特定情况是因为我有一个A类实例,该实例在构造函数中使用B类,然后创建了侦听B的C类实例。我始终将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树。这基本上是事件系统的工作。您给一个孩子的父母,孩子将自己添加到父母,然后可能会或可能不会维护对父母的引用。一棵树是一个简单的例子,但实际上,您可能会发现自己拥有复杂的图形,尽管很少。

在这种情况下,Filter以匿名函数的形式将对自身的引用添加到stdin中,该匿名函数按作用域间接引用Filter。范围引用是要注意的事情,可能会非常复杂。功能强大的GC可以做一些有趣的事情来消除范围变量中的项,但这是另一个主题。关键要理解的是,当您创建一个匿名函数并将其作为ab observable的侦听器添加到某个对象时,observable将维护对该函数以及函数在其上方范围中引用的任何内容的引用(该定义已在)也将得到维护。这些视图具有相同的功能,但是在执行其构造函数后,子级不会保留对其父级的引用。

如果我将上面声明的任何或所有var设置为null,则不会对任何内容产生影响(类似于完成“主要”范围时的情况)。它们仍将处于活动状态,并将数据从stdin传递到stdout和stderr。

如果我将它们全部设置为null,则不可能在不清除stdin上的事件或将stdin设置为null的情况下就将它们删除或GC,(假设可以这样释放它们)。如果代码的其余部分需要标准输入,并且其他重要事件禁止您执行上述操作,那么基本上是这样的内存泄漏,实际上是孤立对象。

为了摆脱df,v1和v2,我需要分别对它们调用一次destroy方法。在实现方面,这意味着Filter和View方法都需要保留对它们创建的匿名侦听器函数以及可观察到的引用的引用,并将其传递给removeListener。

附带说明一下,或者,您可以有一个obserable,它返回一个索引以跟踪侦听器,以便您可以添加原型函数,至少在我看来,这些函数应该在性能和内存上要好得多。但是,您仍然必须跟踪返回的标识符,并传递您的对象以确保侦听器在被调用时绑定到该标识符。

破坏功能会增加一些麻烦。首先,我必须调用它并释放引用:

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

这是一个小麻烦,因为它需要更多代码,但这并不是真正的问题。当我将这些引用传递给许多对象时。在这种情况下,您确切叫什么时候销毁?您不能简单地将这些交给其他对象。您将最终获得销毁链,并通过程序流程或其他方式手动执行跟踪。你无法解雇并忘记。

这种问题的一个示例是,如果我确定View在销毁时也会在df上调用destroy。如果v2仍然存在,则销毁df会破坏它,因此销毁不能简单地传递给df。相反,当v1使用df来使用它时,它将需要告诉df它被使用,这将引发一些计数器或类似于df的情况。df的destroy函数将比counter减小,并且只有在为0时才会真正销毁。这种事情增加了很多复杂性,并且增加了很多可能出错的地方,其中最明显的就是销毁某些东西,而周围仍然有一个引用。将使用循环引用(此时不再是管理计数器的情况,而是引用对象的映射)。当您考虑在JS中实现自己的参考计数器,MM等时,

如果WeakSet是可迭代的,则可以使用:

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

在这种情况下,拥有类还必须保留对f的令牌引用,否则它将变得po琐。

如果使用Observable代替EventListener,则关于事件侦听器的内存管理将是自动的。

不必在每个对象上调用destroy即可完全删除它们:

df = v1 = v2 = null;

如果您未将df设置为null,则它仍然存在,但是v1和v2会自动取消连接。

但是,这种方法有两个问题。

问题之一是它增加了新的复杂性。有时人们实际上并不想要这种行为。我可以创建一个很大的对象链,这些对象通过事件而不是包含(构造函数作用域或对象属性中的引用)相互链接。最终,只有一棵树,我只需要绕过根部而不必担心。释放根将方便地释放整个东西。这两种行为都取决于编码样式等,它们都很有用,并且在创建可重用对象时,很难知道人们想要什么,他们做了什么,您做了什么以及为完成工作而苦恼。如果我使用Observable而不是EventListener,则df要么需要引用v1和v2,要么如果我想将引用的所有权转移到其他超出范围的对象,则必须全部传递它们。诸如此类的弱引用可以通过将控制权从Observable转移给观察者来减轻问题,但不能完全解决(需要检查自身的每个发射或事件)。我猜想,如果该行为仅适用于孤立的图,则该问题将得到解决,这会使GC严重复杂化,而不适用于图外实际上没有引用的引用(仅消耗CPU周期,不进行任何更改)的情况。

问题二是要么在某些情况下是不可预测的,要么强制JS引擎遍历那些按需使用的对象的GC图形,这可能会对性能产生可怕的影响(尽管它很聪明,但可以通过按每个对象执行操作来避免按成员执行操作改为使用WeakMap循环)。如果内存使用量未达到特定阈值且事件不会被删除,则GC可能永远不会运行。如果我将v1设置为null,它可能仍会永远中继到stdout。即使确实获得了GC,这也是任意的,它可能会继续中继到stdout任意时间(1行,10行,2.5行等)。

WeakMap在不可迭代时不关心GC的原因是,访问一个对象无论如何都必须对其进行引用,这样就不会对其进行GC或未将其添加到地图中。

我不确定我对这种事情的看法。您有点无法通过可迭代的WeakMap方法来修复内存管理。析构函数也可能存在第二个问题。

所有这些都会引起地狱的多个层次,因此我建议尝试通过良好的程序设计,良好实践,避免某些事情等来解决它。在JS中,这可能会令人沮丧,因为它在某些方面具有很高的灵活性,并且它更自然地是异步的,并且是基于事件的,具有大量的控制反转。

还有另一种解决方案相当优雅,但仍然存在一些潜在的严重问题。如果您具有扩展可观察类的类,则可以覆盖事件函数。仅在将事件添加到您自己时,将您的事件添加到其他可观察对象。从您删除所有事件后,再从孩子中删除事件。您也可以创建一个类来扩展您的可观察类,以为您完成此操作。这样的类可以为空值和非空值提供钩子,因此您可以观察自己。这种方法还不错,但也有麻烦。复杂度增加,性能下降。您必须保留对所观察对象的引用。至关重要的是,它也不适用于叶子,但是如果您破坏叶子,至少中间体会自毁。它' 就像链接销毁,但隐藏在您已经要链接的呼叫后面。但是,这是一个很大的性能问题,您可能需要在每次类活动时重新初始化Observable的内部数据。如果此过程花费很长时间,那么您可能会遇到麻烦。

如果您可以迭代WeakMap,则可以组合一些东西(如果没有事件,则切换为Weak,在事件时为Strong),但实际上所做的只是将性能问题放在其他人身上。

当涉及到行为时,可迭代的WeakMap也立即带来烦恼。我在前面简要提到了具有作用域引用和雕刻的函数。如果我实例化了一个子类,该子类在将侦听器'console.log(param)'挂钩到父类的构造函数中无法持久化父类,则当我删除对该子类的所有引用时,可以将其完全释放,因为将匿名函数添加到了父母没有从孩子那里引用任何东西。这留下了关于parent.weakmap.add(child,(param)=> console.log(param))的问题。据我所知,键是弱的,但不是值,因此,softmap.add(object,object)是持久的。这是我需要重新评估的东西。对我来说,如果我处理所有其他对象引用,这看起来像是内存泄漏,但实际上我怀疑它基本上是通过将其视为循环引用来进行管理的。匿名函数维护对父作用域产生的对象的隐式引用,以保持一致性,从而浪费大量内存,或者您的行为因难以预测或管理的环境而异。我认为前者实际上是不可能的。在后一种情况下,如果我在类上有一个仅接受对象并添加console.log的方法,则即使我返回了该函数并维护了一个引用,当我清除对该类的引用时,该方法也会被释放。


ES计划者和设计者之间是否存在任何讨论以最终添加到GC系统中?
CMCDragonkai

4

“析构函数甚至都不会在这里为您提供帮助。事件侦听器本身仍在引用您的对象,因此在取消注册之前,将无法对其进行垃圾回收。”

不是这样 析构函数的目的是允许注册侦听器的项取消注册。一旦对象没有其他引用,它将被垃圾回收。

例如,在AngularJS中,销毁控制器时,它可以侦听destroy事件并对其进行响应。这与自动调用析构函数不同,但是它很接近,并且为我们提供了删除控制器初始化时设置的侦听器的机会。

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



4

如果没有这种机制,这种问题的模式/惯例是什么?

术语“清理”可能更合适,但将使用“析构函数”来匹配OP

假设您完全用“函数”和“ var”编写了一些JavaScript。然后你可以使用编写所有的模式function的框架内,S码try/ catch/finally格。在内部finally执行销毁代码。

而不是用C ++风格编写具有未指定生存期的对象类,然后通过任意作用域和对~()作用域末端的隐式调用(~()在C ++中是析构函数)指定生存期,在此javascript模式中,对象是函数,作用域恰好功能范围,而析构函数是finally块。

如果您现在由于try/ catch/finally不包含javascript必需的异步执行而认为这种模式具有固有的缺陷,那么您是正确的。幸运的是,自2018异步编程辅助对象Promise已经有了一个原型功能finally添加到现有resolvecatch原型功能。这意味着需要析构函数的异步作用域可以与Promise对象一起finally用作析构函数来编写。此外,可以在带有s或不带有s的调用中使用try/ catch/ ,但必须注意finallyasync functionPromiseawaitPromise不经等待调用的s将在范围外异步执行,因此在final中处理解扰器代码then

在以下代码中PromiseAPromiseB是一些未finally指定函数参数的旧式API级别的承诺。 PromiseC是否有定义的final参数。

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不需要手动进行垃圾回收,但是异步作用域终止通常是(取消)注册事件侦听器等的地方。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.