【问题标题】:ECMAScript 6 class destructorECMAScript 6 类析构函数
【发布时间】:2015-06-02 16:33:15
【问题描述】:

我知道 ECMAScript 6 有构造函数,但是 ECMAScript 6 有析构函数吗?

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

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

但我希望 ECMAScript 6 有一些本机的东西,可以在对象被垃圾回收之前调用。

如果没有这样的机制,这些问题的模式/约定是什么?

【问题讨论】:

  • 我不会说析构函数是解决这个问题的正确位置。垃圾收集器不会在您期望的时候收集内存,因此,我认为仍然需要某种手动处理。
  • 如果你有事件监听器,你的对象在它们消失之前不能被 GC'd。没有这样的功能有用的场景。
  • 谢谢你们。但是,如果 ECMAScript 没有析构函数,那么好的约定是什么?我应该创建一个名为destructor 的方法并在我完成对象后手动调用它吗?还有其他想法吗?
  • 将事件处理程序绑定部分分成两个方法:this.subscribe() 和 this.unsubscribe()

标签: javascript ecmascript-6


【解决方案1】:

ECMAScript 6 有析构函数这样的东西吗?

没有。 EcmaScript 6 根本没有指定任何垃圾回收语义[1],因此也没有像“破坏”这样的东西。

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

析构函数在这里甚至帮不了你。事件侦听器本身仍然引用您的对象,因此在未注册之前无法进行垃圾收集。
您实际上正在寻找的是一种注册侦听器而不将它们标记为活动根对象的方法。 (请向您当地的事件源制造商咨询此类功能)。

1):嗯,有一个从WeakMap 和WeakSet 对象的规范开始。然而,真正的弱引用仍在管道中[1][2]。

【讨论】:

  • 大妈,期待这些真正的 Javascript 析构函数。
  • 这将非常有帮助。类似于 React 中的对象WillUnmount
  • @nurettin 这与析构函数无关?! (顺便说一句,你使用的是 ES8 async/await,而不是 ES9 Promise.prototype.finally。)
【解决方案2】:

我刚刚在搜索关于析构函数的过程中遇到了这个问题,我认为你的问题在你的 cmets 中有一个未回答的部分,所以我想我会解决这个问题。

谢谢你们。但是如果 ECMAScript 会是一个好的约定 没有析构函数?我应该创建一个名为析构函数的方法吗 并在我完成对象后手动调用它?还有什么想法吗?

如果你想告诉你的对象你现在已经完成了它并且它应该专门释放它拥有的任何事件侦听器,那么你可以创建一个普通的方法来做到这一点。您可以调用类似release() 或deregister() 或unhook() 或类似名称的方法。这个想法是你告诉对象将自己与它所连接的任何其他东西断开连接(取消注册事件侦听器,清除外部对象引用等......)。您必须在适当的时候手动调用它。

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

ES6 确实有weakMap 和weakSet,它们是跟踪一组仍然活着的对象而不影响它们何时可以被垃圾收集的方法,但是当它们被垃圾收集时它不提供任何类型的通知。它们只是在某个时间点从 weakMap 或 weakSet 中消失(当它们被 GC 时)。


仅供参考,您要求的这种类型的析构函数的问题(可能是为什么没有太多要求)是因为垃圾收集,当一个项目有一个打开时,它不符合垃圾收集的条件针对活动对象的事件处理程序,因此即使存在这样的析构函数,在您实际删除事件侦听器之前,它也永远不会在您的情况下被调用。而且,一旦您删除了事件侦听器,就不需要为此目的使用析构函数。

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


仅供参考,这是另一个相关问题Why is the object destructor paradigm in garbage collected languages pervasively absent?。本讨论涵盖终结器、析构器和处置器设计模式。我发现了解这三者之间的区别很有用。


2020 年编辑 - 对象终结器提案

有一个Stage 3 EMCAScript proposal 在对象被垃圾回收后添加一个用户定义的终结器函数。

一个可以从这样的特性中受益的典型例子是一个包含打开文件句柄的对象。如果对象被垃圾回收(因为没有其他代码仍然引用它),那么这个终结器方案允许至少向控制台发送一条消息,表明外部资源刚刚被泄露,并且其他地方的代码应该被修复以防止这个泄漏。

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

【讨论】:

    【解决方案3】:

    您必须在 JS 中手动“破坏”对象。创建销毁函数在 JS 中很常见。在其他语言中,这可能被称为 free、release、dispose、close 等。根据我的经验,虽然它往往是 destroy ,这将解开内部引用、事件并可能将destroy调用传播到子对象。

    WeakMaps 在很大程度上是无用的,因为它们不能被迭代,而且如果有的话,这可能要到 ECMA 7 才能使用。 WeakMaps 让你做的所有事情都是从对象本身分离不可见的属性,除了通过对象引用和 GC 进行查找,这样它们就不会干扰它。这对于缓存、扩展和处理复数很有用,但它对可观察对象和观察者的内存管理并没有真正的帮助。 WeakSet 是 WeakMap 的子集(类似于默认值为 boolean true 的 WeakMap)。

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

    析构函数实际上对观察者/侦听器也可能无用,因为通常侦听器将直接或间接持有对观察者的引用。析构函数仅在没有弱引用的情况下真正以代理方式工作。如果您的 Observer 真的只是一个代理,接收其他东西的 Listeners 并将它们放在一个 observable 上,那么它可以在那里做一些事情,但这种事情很少有用。析构函数更多用于 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 但它不会工作,因为它们已经创建了一个以标准输入为根的树。这基本上就是事件系统所做的。你给一个孩子一个父母,孩子将自己添加到父母中,然后可能会或可能不会维护对父母的引用。树是一个简单的例子,但实际上你也可能会发现自己有复杂的图表,尽管很少。

    在这种情况下,Filter 以匿名函数的形式向标准输入添加对自身的引用,该函数通过作用域间接引用 Filter。范围引用是需要注意的,并且可能非常复杂。强大的 GC 可以做一些有趣的事情来分割范围变量中的项目,但这是另一个主题。理解的关键是,当您创建一个匿名函数并将其作为监听器添加到某事物中时,可观察对象将维护对该函数的引用以及该函数在其上方范围内引用的任何内容(它在) 也将被保留。视图执行相同的操作,但在执行其构造函数后,子级不会维护对其父级的引用。

    如果我将上面声明的任何或所有变量设置为 null,它不会对任何事情产生影响(类似地,当它完成那个“主”范围时)。它们仍将处于活动状态,并将数据从标准输入传送到标准输出和标准错误。

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

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

    顺便说一句,您也可以有一个 oberable,它返回一个索引来跟踪侦听器,这样您就可以添加原型函数,至少在我看来,这些函数在性能和内存方面应该会更好。不过,您仍然必须跟踪返回的标识符并传递您的对象以确保在调用时将侦听器绑定到它。

    destroy 函数会增加一些麻烦。首先是我必须调用它并释放引用:

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

    这是一个小烦恼,因为它的代码有点多,但这不是真正的问题。当我将这些引用传递给许多对象时。在这种情况下,您究竟什么时候调用破坏?您不能简单地将这些交给其他对象。您最终将通过程序流或其他方式获得破坏链和手动实现跟踪。你不能开枪就忘记。

    此类问题的一个示例是,如果我决定 View 在 df 被销毁时也会调用 destroy 。如果 v2 仍在销毁 df 将破坏它,因此不能简单地将销毁转发给 df。相反,当 v1 使用 df 使用它时,它需要告诉 df 它已被使用,这将引发一些计数器或类似于 df。 df 的销毁函数会比计数器减少,并且只有在它为 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 的标记引用,否则它将失败。

    如果使用 Observable 而不是 EventListener,那么对于事件侦听器,内存管理将是自动的。

    与其对每个对象调用destroy,这足以完全删除它们:

    df = v1 = v2 = null;
    

    如果您没有将 df 设置为 null,它仍然存在,但 v1 和 v2 会自动取消挂钩。

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

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

    问题二是在某些情况下它是不可预测的,或者迫使 JS 引擎根据需要遍历那些对象的 GC 图,这可能会产生可怕的性能影响(尽管如果它很聪明,它可以避免每个成员都这样做而是按照 WeakMap 循环执行此操作)。如果内存使用量没有达到某个阈值并且对象及其事件不会被删除,则 GC 可能永远不会运行。如果我将 v1 设置为 null,它可能仍会永远中继到标准输出。即使它确实得到了 GC,这也是任意的,它可能会继续中继到标准输出任意时间(1 行、10 行、2.5 行等)。

    WeakMap 在不可迭代时不关心 GC 的原因是,要访问一个对象,无论如何你都必须引用它,所以要么它没有被 GC,要么没有被添加到地图中.

    我不确定我对这种事情的看法。您有点破坏内存管理以使用可迭代的 WeakMap 方法修复它。析构函数也可能存在问题二。

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

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

    如果您可以迭代 WeakMap,那么您也许可以组合事物(在没有事件时切换到弱,在事件时切换到强),但真正要做的就是将性能问题放在其他人身上。

    当涉及到行为时,可迭代的 WeakMap 也有直接的烦恼。我之前简要提到过具有范围引用和雕刻的函数。如果我在将侦听器“console.log(param)”挂钩到父级的构造函数中实例化一个子级并且无法持久化父级,那么当我删除对子级的所有引用时,它可以完全释放,因为匿名函数添加到parent 没有从孩子内部引用任何内容。这就留下了如何处理 parent.weakmap.add(child, (param) => console.log(param)) 的问题。据我所知,键是弱的,但不是值,所以 weakmap.add(object, object) 是持久的。这是我需要重新评估的东西。对我来说,如果我处理所有其他对象引用,这看起来像是内存泄漏,但我怀疑实际上它基本上通过将其视为循环引用来管理它。匿名函数要么维护对从父范围产生的对象的隐式引用,以保持一致性,从而浪费大量内存,要么您的行为根据难以预测或管理的情况而变化。我认为前者实际上是不可能的。在后一种情况下,如果我在一个类上有一个方法,它只接受一个对象并添加 console.log,即使我返回了该函数并维护了一个引用,当我清除对该类的引用时,它也会被释放。公平地说,这种特殊场景很少合法地需要,但最终有人会找到一个角度并要求一个可迭代的 HalfWeakMap(释放键和值引用时免费)但也是不可预测的(obj = null 神奇地结束 IO, f = null 神奇地结束 IO,两者都可以在难以置信的距离上实现)。

    【讨论】:

    • ES 规划者和设计者之间是否有任何讨论最终将挂钩添加到 GC 系统中?
    【解决方案4】:

    如果没有这样的机制,这些问题的模式/约定是什么?

    “cleanup”这个词可能更合适,但会使用“destructor”来匹配 OP

    假设您编写了一些完全使用 'function's 和 'var's 的 javascript。 然后你可以使用在try/catch/finally格的框架内编写所有functions代码的模式。在finally 内执行销毁代码。

    而不是 C++ 风格编写具有未指定生命周期的对象类,然后通过任意范围指定生命周期并在范围结束时隐式调用 ~()(~() 是 C++ 中的析构函数),在这个 javascript 模式中object 是函数,作用域就是函数作用域,析构函数是finally 块。

    如果您现在认为这种模式存在固有缺陷,因为try/catch/finally 不包含对 javascript 必不可少的异步执行,那么您是正确的。幸运的是,自 2018 年以来,异步编程帮助器对象 Promise 在现有的 resolve 和 catch 原型函数中添加了原型函数 finally。这意味着需要析构函数的异步作用域可以用Promise 对象编写,使用finally 作为析构函数。此外,您可以在async function 调用Promises 中使用try/catch/finally,有或没有await,但必须注意,在没有等待的情况下调用Promises 将在外部异步执行范围,因此在最终的then 中处理析构函数代码。

    在以下代码中,PromiseA 和 PromiseB 是一些未指定 finally 函数参数的旧 API 级别承诺。 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 */ }
    }
    

    我并不是提倡将 javascript 中的每个对象都写成一个函数。相反,请考虑这样一种情况:您确定了一个范围,该范围确实“希望”在其生命周期结束时调用析构函数。将该范围定义为函数对象,使用模式的 finally 块(或在异步范围的情况下为 finally 函数)作为析构函数。很可能制定该函数对象消除了对非函数类的需求,否则该类将被编写 - 不需要额外的代码,对齐范围和类甚至可能更清晰。

    注意:正如其他人所写,我们不应该混淆析构函数和垃圾收集。碰巧的是,C++ 析构函数通常或主要关注手动垃圾回收,但并非专门如此。 Javascript 不需要手动垃圾收集,但异步范围的生命周期结束通常是(取消)注册事件侦听器等的地方。

    【讨论】:

      【解决方案5】:

      “析构函数甚至帮不了你。它是事件监听器 他们自己仍然引用你的对象,所以它不能 在未注册之前进行垃圾收集。”

      并非如此。析构函数的目的是允许注册监听器的项目取消注册它们。一旦一个对象没有其他对它的引用,它将被垃圾收集。

      例如,在 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() );
      }
      
      
      

      【讨论】:

        【解决方案6】:

        其他答案已经详细解释了没有析构函数。但是您的实际目标似乎与事件有关。您有一个连接到某个事件的对象,并且您希望此连接在对象被垃圾收集时自动消失。但这不会发生,因为事件订阅本身引用了侦听器函数。好吧,除非你使用这个漂亮的新 WeakRef 东西。

        这是一个例子:

        <!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)

        点击订阅按钮会创建一个新的监听函数,并在Emit按钮的点击事件中注册它。因此,单击 Emit 按钮后会向控制台打印一条消息。现在单击 Free 按钮,它只是将侦听器变量设置为 null,以便垃圾收集器可以删除侦听器。等待一段时间或在开发者控制台中强制收集垃圾,然后再次单击 Emit 按钮。包装侦听器函数现在看到实际侦听器(包装在 WeakRef 中)不再存在,然后从按钮取消订阅。

        WeakRefs 非常强大,但请注意,无法保证您的内容是否以及何时被垃圾回收。

        【讨论】:

          【解决方案7】:

          Javascript 没有像 C++ 那样的解构。相反,应该使用替代设计模式来管理资源。以下是几个例子:

          您可以限制用户在回调期间使用该实例,之后会自动清理该实例。 (这种模式类似于 Python 中深受喜爱的“with”语句)

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

          当上面的例子过于严格时,另一种选择是只创建显式的清理函数。

          const db = makeDatabaseConnection()
          
          const resource = await db.doSomeRequest()
          updatePageWithResource(resource)
          
          pageChangeEvent.addListener(() => {
            db.destroy()
          })
          

          【讨论】:

            【解决方案8】:

            给你。如果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);
                  }
                },
              };
            };
            

            虽然它可能不会在回调函数超出范围后立即发生,或者根本不会发生。有关更多详细信息,请参阅weakRef 文档。但它对我的用例来说就像一个魅力。

            您可能还想查看FinalizationRegistry API 以了解不同的方法。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2018-07-09
              • 2018-01-18
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2022-01-05
              相关资源
              最近更新 更多