ECMAScript 6类析构函数

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

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

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

但是我希望 ECMAScript 6能够在对象被垃圾收集之前调用一些本机的东西。

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

149713 次浏览

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

EcmaScript6没有在 all[1]上指定任何垃圾收集语义,所以也没有什么类似于“销毁”的东西。

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

一个破坏者都帮不了你。仍然引用您的对象的是事件侦听器本身,因此在它们未注册之前不能进行垃圾收集。
实际上,您正在寻找的是一种注册侦听器而不将它们标记为活动根对象的方法。(向您的本地事件源制造商索要这样的特性)。

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

我在搜索关于析构函数时偶然发现了这个问题,我认为在你的评论中有一部分问题没有得到回答,所以我想我应该解决这个问题。

谢谢你们,但是如果 ECMAScript 没有析构函数吗? 我应该创建一个名为析构函数的方法吗 手动调用它,当我完成对象? 还有其他办法吗?

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

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

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


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

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


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


2020年编辑-对象终结器的建议

在对象被垃圾收集之后,有一个 第三阶段 EMCAScript 建议用于添加用户定义的终结器函数。

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

如果您仔细阅读这个提案,您会发现它完全不像 C + + 这样的语言中的成熟析构函数。在对象已经被销毁之后调用这个终结器,您必须预先确定需要将实例数据的哪一部分传递给终结器,以便它执行其工作。此外,这个特性并不能用于正常操作,而是作为一个调试帮助和一个针对某些类型的 bug 的后台。您可以阅读提案中对这些限制的完整解释。

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

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

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

实际上,析构函数对于观察器/侦听器也可能没有用处,因为通常侦听器将直接或间接地保存对观察器的引用。析构函数只能在没有弱引用的情况下以代理方式工作。如果你的观察者实际上只是一个代理,把别人的监听器放到一个可观察的监听器上,那么它可以在那里做一些事情,但是这种事情很少有用。析构函数主要用于 IO 相关的事情或者在包含范围之外的事情(IE,链接它创建的两个实例)。

我开始研究这个特定的情况是因为我有一个 A 类实例,它在构造函数中接受 B 类,然后创建一个 C 类实例,它监听 B。我总是把 B 实例放在上面的某个位置。A 我有时候会扔掉,创造新的,创造很多,等等。在这种情况下,一个析构函数实际上可以为我工作,但是有一个讨厌的副作用: 如果我在父代中传递 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 by scope。范围引用是需要注意的,它可能非常复杂。一个强大的 GC 可以做一些有趣的事情来去除范围变量中的项,但这是另一个话题。要理解的关键是,当你创建一个匿名函数并将其作为监听器添加到 ab 观察器时,观察器将维护对该函数的引用,并且在它上面的作用域(它是在其中定义的)中的任何函数引用也将得到维护。视图也执行相同的操作,但是在执行其构造函数之后,子视图不维护对其父视图的引用。

如果我将上面声明的任何或所有 var 设置为 null,那么它对任何内容都不会产生影响(类似地,当它完成“ main”作用域时)。它们仍然是活动的,并将数据从 stdin 传输到 stdout 和 stderr。

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

为了去掉 df,v1和 v2,我需要对它们各自调用一个销毁方法。在实现方面,这意味着 Filter 和 View 方法都需要保留对它们创建的匿名侦听器函数的引用以及可观察的引用,并将其传递给 RemoveListener。

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

销毁函数增加了一些麻烦。首先,我必须调用它并释放引用:

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

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

这类问题的一个例子是,如果我决定 View 在 df 被销毁时也会调用 delete。如果 v2仍然存在,则销毁 df 将破坏它,因此销毁不能简单地转发给 df。相反,当 v1使用 df 时,它需要告诉 df 它被使用了,这会产生一些计数器或类似于 df 的值。Df 的销毁函数会比 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 的令牌引用,否则它就会消失。

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

不需要对每个物体调用销毁,这样就足以完全移除它们:

df = v1 = v2 = null;

如果没有将 df 设置为 null,那么它仍然存在,但是 v1和 v2会自动解除挂钩。

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

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

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

WeakMap 在不可迭代时不关心 GC 的原因是,要访问一个对象,无论如何都必须有一个对它的引用,因此它要么没有被 GCed,要么没有被添加到映射中。

我不知道我对这种事是怎么想的。您在某种程度上破坏了内存管理,使用可迭代的 WeakMap 方法来修复它。对于析构函数也可能存在问题2。

所有这些都会带来一些麻烦,所以我建议你试着用好的程序设计、好的实践、避免某些事情等等来解决这些问题。然而,在 JS 中它可能会令人沮丧,因为它在某些方面非常灵活,而且更自然地是异步的,基于事件的控制反转很大。

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

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

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

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

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

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




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

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

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

代替 C + + 风格的写对象类与未指定的生存期,然后指定生存期的任意作用域和隐式调用 ~()在范围结束(~()是 C + + 的析构函数) ,在这个 javascript 模式的对象是函数,范围正好是函数范围,和析构函数是 finally块。

如果你现在认为这个模式本质上是有缺陷的,因为 try/catch/finally不包含异步执行,而异步执行对 javascript 来说是必不可少的,那么你是对的。幸运的是,自2018年以来,异步编程助手对象 Promise在已经存在的 resolvecatch原型函数的基础上添加了一个原型函数 finally。这意味着需要析构函数的异步作用域可以用 Promise对象编写,使用 finally作为析构函数。此外,您可以在有或没有 catch4的 catch2调用 Promise中使用 try/catch/finally,但是必须注意,在没有等待的情况下调用的 Promise将在作用域外异步执行,因此在最终的 catch6中处理解析器代码。

在以下代码中,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 */ }
}

我并不主张把 javascript 中的每个对象都写成一个函数。相反,考虑这样一种情况,即确定了一个范围,该范围真正“希望”在生命周期结束时调用析构函数。使用模式的 finally块(在异步作用域的情况下是 finally函数)作为析构函数,将该作用域表示为函数对象。很有可能的情况是,构造函数对象可以避免编写非函数类的需要——不需要额外的代码,对齐范围和类甚至可能更简单。

注意: 正如其他人所写的,我们不应该混淆析构函数和垃圾收集。碰巧,C + + 析构函数通常或主要与手动垃圾收集有关,而 独家则不然。Javascript 不需要手动垃圾收集,但异步作用域结束时通常是注册事件侦听器等的地方。.

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

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

这里有一个例子:

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

单击 订阅按钮将创建一个新的侦听器函数,并在 发射按钮的 click 事件中注册该函数。因此,单击之后的 发射按钮将向控制台打印一条消息。现在单击 自由按钮,该按钮将侦听器变量设置为 null,这样垃圾收集器就可以删除侦听器。等待一段时间或强制在开发人员控制台中收集 gargabe,然后再次单击 发射按钮。包装侦听器函数现在看到实际的侦听器(包装在 WeakRef 中)不再存在,然后从按钮中取消订阅。

WeakReff 非常强大,但是请注意,如果以及何时您的内容被垃圾收集,则无法保证。

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

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

您可能还希望查看 终结注册表 API 以获得不同的方法。

标题中提到的问题的答案是 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;
}
}

注意:

尽可能避免

正确使用 FinalizationRegistry 需要仔细考虑,如果可能的话,最好避免使用它。何时、如何以及是否发生垃圾收集取决于任何给定 JavaScript 引擎的实现。您在一个引擎中观察到的任何行为可能在另一个引擎中、在同一引擎的另一个版本中、甚至在同一引擎的同一版本中略有不同的情况下都是不同的。

...

开发人员不应该依赖清理回调来实现必要的程序逻辑。清理回调可能有助于在整个程序过程中减少内存使用,但在其他情况下不太可能有用。

调用清理回调不需要符合规范的 JavaScript 实现,即使是执行垃圾收集的 JavaScript 实现。何时以及是否这样做完全取决于 JavaScript 引擎的实现。当一个已注册的对象被回收时,它的任何清理回调都可能在那时或者一段时间之后被调用 或者根本不需要。

Mozilla Developer Network