ECMAScript 6类析构函数

Ale*_*ack 52 javascript ecmascript-6

我知道ECMAScript 6有构造函数,但ECMAScript 6的析构函数是否存在?

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

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

但是我希望ECMAScript 6能够在对象被垃圾收集之前调用本机.

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

jfr*_*d00 24

我刚刚在搜索析构函数时遇到了这个问题,我认为你的评论中有一个未回答的问题部分,所以我想我会解决这个问题.

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

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

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

ES6确实有weakMap和weakSet,它们是跟踪一组仍然存活的对象而不影响它们何时可以被垃圾收集的方式,但是当它们被垃圾收集时它不提供任何类型的通知.它们在某些时刻(当它们被GCed时)从weakMap或weakSet中消失.


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

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


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


Ber*_*rgi 20

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

不,EcmaScript 6根本没有指定任何垃圾收集语义[1],所以也没有像"破坏"那样的东西.

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

析构函数甚至不会帮助你.事件监听器本身仍然引用您的对象,因此在取消注册之前它将无法进行垃圾收集.
您实际需要的是一种注册侦听器而不将其标记为实时根对象的方法.(向您当地的事件源制造商咨询此类功能).

1):嗯,是的规范开始WeakMapWeakSet对象.然而,真正的弱引用仍在进行中[1] [2].

  • 大坝,期待这些真正的Javascript析构函数. (7认同)

jgm*_*jgm 12

你必须在JS中手动"破坏"对象.在JS中创建销毁函数很常见.在其他语言中,这可能被称为自由,释放,处置,关闭等.在我的经​​验中,虽然它往往是破坏,它将取消内部引用,事件和可能的传播也会破坏对子对象的调用.

WeakMaps在很大程度上是无用的,因为它们无法迭代,这可能在ECMA 7之前无法使用.所有WeakMaps让你做的是从对象本身分离出不可见的属性,除了通过对象引用和GC查找,这样它们就不会打扰它.这对于缓存,扩展和处理多个数据非常有用,但它对于可观察者和观察者的内存管理并没有实际帮助.WeakSet是WeakMap的子集(类似于WeakMap,默认值为boolean true).

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

对于观察者/侦听器,析构函数实际上也是无用的,因为侦听器通常会直接或间接地保存对观察者的引用.析构函数只能在没有弱引用的情况下以代理方式工作.如果你的观察者真的只是一个代理人拿一些别人的听众并将它们放在一个可观察的东西上,那么它可以在那里做点什么,但这种事情很少有用.析构函数更多用于IO相关事物或在包含范围之外的事情(IE,连接它创建的两个实例).

我开始研究这个问题的具体情况是因为我有一个在构造函数中接受类B的类A实例,然后创建监听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.
    });
}
Run Code Online (Sandbox Code Playgroud)

另一方面,如果没有匿名/独特的功能,很难让事情发挥作用,这些功能将在稍后介绍.

在正常情况下,实例化将如此(伪):

var df = new Filter(stdin),
    v1 = new View(df, stdout),
    v2 = new View(df, stderr);
Run Code Online (Sandbox Code Playgroud)

通常你会将它们设置为null但它不会工作,因为它们在根处创建了一个带有stdin的树.这基本上就是事件系统的作用.您将父项提供给子项,子项将其自身添加到父项,然后可能会或可能不会保留对父项的引用.树是一个简单的例子,但实际上你也可能发现自己有复杂的图形,尽管很少.

在这种情况下,Filter以匿名函数的形式向stdin添加对stdin的引用,该函数间接引用Filter by scope.范围引用是需要注意的,并且可能非常复杂.一个功能强大的GC可以做一些有趣的事情来删除范围变量中的项目,但这是另一个主题.理解的关键是当你创建一个匿名函数并将其作为ab observable的监听器添加到某个东西时,observable将维护对函数的引用以及函数在其上面的作用域中引用的任何内容(它在)也将保持.视图执行相同但在执行其构造函数后,子项不会保留对父项的引用.

如果我将上面声明的任何或所有变量设置为null,则不会对任何事物产生影响(类似于它完成"主"范围时).它们仍然是活动的,并将数据从stdin传递到stdout和stderr.

如果我将它们全部设置为null,则在不清除stdin上的事件或将stdin设置为null(假设它可以像这样释放)的情况下,将它们移除或GCed是不可能的.如果代码的其余部分需要stdin并且其上有其他重要事件阻止您执行上述操作,那么您实际上有一个内存泄漏实际上是孤立对象.

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

另外,您也可以使用一个可以返回索引来跟踪侦听器,以便您可以添加原型函数,这至少对我的理解应该在性能和内存方面要好得多.您仍然必须跟踪返回的标识符并传递您的对象以确保在调用时绑定器绑定它.

破坏功能增加了几个痛苦.首先是我必须调用它并释放引用:

df.destroy();
v1.destroy();
v2.destroy();
df = v1 = v2 = null;
Run Code Online (Sandbox Code Playgroud)

这是一个小麻烦,因为它是更多的代码,但这不是真正的问题.当我将这些引用传递给许多对象时.在这种情况下,你究竟什么时候叫毁灭?你不能简单地把它们交给其他物体.您将通过程序流程或其他方式最终获得破坏链和手动跟踪实施.你不能开火和忘记.

这种问题的一个例子是,如果我确定View在df被销毁时也会调用destroy.如果v2仍然在破坏df将打破它,所以销毁不能简单地转发到df.相反,当v1使用df来使用它时,它需要告诉df它被用来提升一些计数器或类似于df.df的销毁功能会比计数器减少而且实际上只有0才会消失.这种事情增加了很多复杂性并增加了许多可能出错的问题,其中最明显的就是破坏某些东西,而某些地方仍然存在参考将使用和循环引用(此时它不再是管理计数器的情况,而是引用对象的映射).当您考虑在JS中实现自己的引用计数器,MM等时,它可能不足.

如果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);
};
Run Code Online (Sandbox Code Playgroud)

在这种情况下,拥有类还必须保留对f的标记引用,否则它将变为poof.

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

而不是在每个对象上调用destroy,这足以完全删除它们:

df = v1 = v2 = null;
Run Code Online (Sandbox Code Playgroud)

如果你没有将df设置为null,它仍然存在,但v1和v2将自动解除挂钩.

然而,这种方法存在两个问题.

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

问题二是要么在某些情况下是不可预测的,要么迫使JS引擎遍历GC图表以查找那些可能产生可怕性能影响的对象(尽管如果它很聪明,它可以避免每个成员按照每个成员执行它而是WeakMap循环).如果内存使用量未达到某个阈值,则GC可能永远不会运行,并且不会删除包含其事件的对象.如果我将v1设置为null,它仍然可以永久地转发到stdout.即使它确实得到GCed,这将是任意的,它可以继续传递到stdout任何时间(1行,10行,2.5行等).

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

我不确定我对这种事情的看法.您可以通过可迭代的WeakMap方法来破坏内存管理.问题二也可以存在于析构函数中.

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

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

如果您可以迭代WeakMap,那么您可以组合事物(当没有事件时切换到Weak,当事件时切换为Strong)但是所有真正做的就是将性能问题放在其他人身上.

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


Igo*_*rev 6

干得好。如果该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);
      }
    },
  };
};
Run Code Online (Sandbox Code Playgroud)

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

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


Cra*_*cks 5

如果没有这样的机制,那么针对此类问题的模式/约定是什么?

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

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

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

如果你现在想这种模式固有的缺陷,因为try/ catch/finally不包括异步执行这是JavaScript的必要,那么你是正确的。幸运的是,自2018异步编程辅助对象Promise已经有了一个原型功能finally添加到现有resolvecatch原型功能。这意味着需要析构函数的异步作用域可以用Promise对象编写,finally用作析构函数。此外,你还可以使用try/ catch/finallyasync function调用Promise带或不带小号await,但必须认识到,Promise在没有 await 的情况下调用的 s 将在作用域外异步执行,因此在 final 中处理析构函数代码then

在下面的代码中PromiseAPromiseB是一些没有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 */ }
}
Run Code Online (Sandbox Code Playgroud)

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

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