ECMAScript 6 class 析构函数
ECMAScript 6 class destructor
我知道 ECMAScript 6 有构造函数,但 ECMAScript 6 有析构函数吗?
例如,如果我在构造函数中将我对象的一些方法注册为事件侦听器,我想在我的对象被删除时删除它们。
一个解决方案是为每个需要这种行为的 class 创建一个 destructor
方法并手动调用它。这将删除对事件处理程序的引用,因此我的对象将真正准备好进行垃圾回收。否则它会因为这些方法而留在内存中。
但我希望 ECMAScript 6 是否有一些原生的东西可以在对象被垃圾回收之前调用。
如果没有这样的机制,对于这样的问题pattern/convention是什么?
Is there such a thing as destructors for ECMAScript 6?
没有。 EcmaScript 6 根本没有指定任何垃圾收集语义[1],所以也没有像 "destruction" 那样的东西。
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
析构函数在这里甚至帮不了你。事件侦听器本身仍然引用您的对象,因此在取消注册之前无法对其进行垃圾收集。
您实际上正在寻找的是一种注册侦听器而不将它们标记为活动根对象的方法。 (向您当地的事件源制造商咨询此类功能)。
1): 嗯,有一个开头的规范WeakMap
and WeakSet
objects. However, true weak references are still in the pipeline [1][2]。
我刚刚在搜索析构函数时遇到了这个问题,我认为您的评论中有部分问题没有得到解答,所以我想我会解决这个问题。
thank you guys. But what would be a good convention if ECMAScript
doesn't have destructors? Should I create a method called destructor
and call it manually when I'm done with the object? Any other idea?
如果你想告诉你的对象你现在已经完成了它并且它应该专门释放它拥有的任何事件侦听器,那么你可以创建一个普通的方法来做到这一点。您可以调用类似 release()
或 deregister()
或 unhook()
之类的方法。这个想法是你告诉对象将自己与它所连接的任何其他东西断开连接(注销事件监听器,清除外部对象引用等)。您将不得不在适当的时候手动调用它。
如果同时您还确保没有其他引用指向该对象,那么您的对象此时将符合垃圾回收条件。
ES6 确实有 weakMap 和 weakSet,它们是跟踪一组仍然存在的对象的方法,而不会影响它们何时可以被垃圾收集,但是当它们被垃圾收集时它不提供任何类型的通知。它们只是在某个时候(当它们被 GC 时)从 weakMap 或 weakSet 中消失。
仅供参考,您要求的这种类型的析构函数的问题(以及可能为什么没有太多要求)是因为垃圾收集,当一个项目有垃圾收集时,它不符合垃圾收集的条件针对活动对象的打开事件处理程序,因此即使有这样的析构函数,在您实际删除事件侦听器之前,它也永远不会在您的情况下被调用。而且,一旦您删除了事件侦听器,就不需要为此目的使用析构函数。
我想有可能 weakListener()
不会阻止垃圾收集,但这样的事情也不存在。
仅供参考,这是另一个相关问题 Why is the object destructor paradigm in garbage collected languages pervasively absent?。本次讨论涵盖终结器、析构器和处理器设计模式。我发现了解这三者之间的区别很有用。
2020 年编辑 - 对象终结器提案
有一个 Stage 3 EMCAScript proposal 可以在对象被垃圾回收后添加一个用户定义的终结器函数。
可以从此类功能中获益的典型示例是包含打开文件句柄的对象。如果对象被垃圾回收(因为没有其他代码仍然引用它),那么这个终结器方案允许至少向控制台发送一条消息,表明外部资源刚刚泄露,其他地方的代码应该被修复以防止这次泄漏。
如果您仔细阅读了该提案,您会发现它完全不像 C++ 等语言中成熟的析构函数。这个终结器在对象已经被销毁之后被调用,你必须预先确定实例数据的哪一部分需要传递给终结器来完成它的工作。此外,此功能并不是正常操作所依赖的,而是作为调试辅助工具和针对某些类型的错误的支持。您可以在提案中阅读对这些限制的完整解释。
您必须在 JS 中手动 "destruct" objects。创建销毁函数在 JS 中很常见。在其他语言中,这可能被称为 free、release、dispose、close 等。根据我的经验,尽管它往往会被销毁,这将解除内部引用、事件的挂钩,并可能传播对 child objects 的销毁调用还有。
WeakMaps 在很大程度上是无用的,因为它们不能被迭代,并且这可能要到 ECMA 7 才可用(如果有的话)。除了 object 引用和 GC 的查找之外,WeakMaps 让你做的就是拥有与 object 本身分离的不可见属性,这样它们就不会干扰它。这对于缓存、扩展和处理复数很有用,但它对可观察对象和观察者的内存管理没有真正帮助。 WeakSet 是 WeakMap 的一个子集(类似于默认值为布尔值 true 的 WeakMap)。
关于是否对 this 或析构函数使用弱引用的各种实现存在各种争论。两者都有潜在的问题,析构函数更受限制。
析构函数实际上对 observers/listeners 也可能无用,因为通常情况下,侦听器将直接或间接持有对观察者的引用。析构函数仅以没有弱引用的代理方式真正起作用。如果你的观察者真的只是一个代理,接受其他东西的听众并将它们放在一个可观察的对象上,那么它可以在那里做一些事情,但这种事情很少有用。析构函数更多的是用于IO相关的事情或者在包含范围之外做事情(IE,链接它创建的两个实例)。
我开始研究这个的具体情况是因为我有 class A 实例,它在构造函数中使用 class B,然后创建 class C 实例来监听B. 我总是把 B 实例放在高处。 A 我有时会扔掉,创建新的,创建很多,等等。在这种情况下,析构函数实际上对我有用,但如果我传递 C 实例但删除所有 A 引用,那么在 parent 中会产生令人讨厌的副作用那么C和B的绑定就会被破坏(C下面的地面被移除了)。
在 JS 中没有自动解决方案是痛苦的,但我认为这不是很容易解决的。考虑这些 classes(伪):
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.
});
}
附带说明一下,如果没有 anonymous/unique 稍后将介绍的功能,很难使事情正常进行。
在正常情况下实例化将是这样(伪):
var df = new Filter(stdin),
v1 = new View(df, stdout),
v2 = new View(df, stderr);
通常要对这些进行 GC,您会将它们设置为空,但它不会起作用,因为它们已经在根部创建了一个带有 stdin 的树。这基本上就是事件系统所做的。您将 parent 赋予 child,child 将自身添加到 parent,然后可能会或可能不会保留对 parent 的引用。树是一个简单的例子,但在现实中你也可能会发现自己有复杂的图形,尽管很少见。
在这种情况下,Filter 以匿名函数的形式将对自身的引用添加到 stdin,该匿名函数按作用域间接引用 Filter。范围引用是需要注意的,而且可能非常复杂。强大的 GC 可以做一些有趣的事情来剥离范围变量中的项目,但这是另一个话题。理解的关键是,当您创建一个匿名函数并将其作为 ab observable 的侦听器添加到某物时,observable 将维护对该函数的引用以及该函数在其上方范围内引用的任何内容(它在) 也将得到维护。视图做同样的事情,但是在执行它们的构造函数之后,children 不维护对它们的 parents 的引用。
如果我将上面声明的任何或所有变量设置为 null,它不会对任何事情产生影响(类似地,当它完成 "main" 范围时)。它们仍将处于活动状态并将数据从 stdin 传输到 stdout 和 stderr。
如果我将它们全部设置为 null,则在不清除 stdin 上的事件或将 stdin 设置为 null(假设它可以像这样释放)的情况下,不可能将它们删除或进行 GC。如果其余代码需要 stdin 并且上面有其他重要事件禁止您执行上述操作,那么您基本上会以这种方式发生内存泄漏,实际上是孤立的 objects。
为了摆脱 df、v1 和 v2,我需要对它们中的每一个调用 destroy 方法。在实现方面,这意味着 Filter 和 View 方法都需要保留对它们创建的匿名侦听器函数以及可观察对象的引用,并将其传递给 removeListener。
附带说明,或者你可以有一个可观察的 returns 一个索引来跟踪听众,这样你就可以添加原型函数,至少据我所知,这在性能和内存方面应该更好.您仍然必须跟踪返回的标识符并传递您的 object 以确保侦听器绑定到它在调用时。
销毁功能增加了一些麻烦。首先是我必须调用它并释放引用:
df.destroy();
v1.destroy();
v2.destroy();
df = v1 = v2 = null;
这是一个小麻烦,因为它的代码有点多,但这不是真正的问题。当我将这些参考资料交给许多 object 时。在这种情况下,你到底什么时候调用销毁?您不能简单地将这些交给其他 object。您最终会遇到一连串的破坏和通过程序流或其他方式手动实施跟踪。你不能一劳永逸。
这种问题的一个例子是,如果我决定 View 在销毁时也会在 df 上调用 destroy。如果 v2 仍然存在,则破坏 df 将破坏它,因此不能简单地将破坏传递给 df。相反,当 v1 使用 df 来使用它时,它需要告诉 df 它被使用,这会引发一些计数器或类似于 df。 df 的 destroy 函数会比 counter 减少,只有当它为 0 时才会真正销毁。这种事情增加了很多复杂性并增加了很多可能出错的地方,其中最明显的是销毁了一些东西,而在某个地方仍然有一个引用将使用和循环引用(此时不再是管理计数器的情况而是引用objects的映射)。当你想在 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);
};
在这种情况下,拥有者 class 还必须保留对 f 的标记引用,否则它会出错。
如果使用 Observable 而不是 EventListener,那么关于事件侦听器的内存管理将是自动的。
与其在每个 object 上调用销毁,这足以完全删除它们:
df = v1 = v2 = null;
如果您没有将 df 设置为 null,它仍然存在,但 v1 和 v2 会自动解除挂钩。
但是这种方法有两个问题。
问题一是它增加了新的复杂性。有时人们实际上并不想要这种行为。我可以创建一个非常大的 object 链,它们通过事件而不是包含(构造函数范围中的引用或 object 属性)相互链接。最终,一棵树和我只需要绕过根部并为此担心。释放根将方便地释放整个东西。取决于编码风格等的两种行为都很有用,并且在创建可重用的 objects 时,很难知道人们想要什么,他们做了什么,你做了什么,并且很难解决已经发生的事情完毕。如果我使用 Observable 而不是 EventListener,那么 df 将需要引用 v1 和 v2,或者如果我想将引用的所有权转移到范围之外的其他对象,我将不得不全部传递它们。类似弱引用的东西可以通过将控制权从 Observable 转移到观察者来稍微缓解这个问题,但不会完全解决它(并且需要检查每个发射或事件本身)。这个问题可以解决我想如果行为只适用于孤立的图,这会使 GC 严重复杂化,并且不适用于图外有引用的情况,这些引用实际上是 noops(只消耗 CPU 周期,不进行了更改)。
问题二是在某些情况下它是不可预测的,或者强制 JS 引擎根据需要遍历那些 objects 的 GC 图,这可能会产生可怕的性能影响(尽管如果它很聪明它可以通过每个 WeakMap 循环来避免对每个成员进行操作)。如果内存使用未达到特定阈值,GC 可能永远不会 运行,并且 object 及其事件不会被删除。如果我将 v1 设置为 null,它可能仍会永远中继到标准输出。即使它确实得到了 GC,这也是任意的,它可能会继续中继到标准输出任何时间(1 行、10 行、2.5 行等)。
当 non-iterable 时 WeakMap 不关心 GC 的原因是要访问 object 无论如何你必须有一个对它的引用所以它没有被 GC 或尚未添加到地图中。
我不确定我对这种事情的看法。您有点破坏内存管理以使用可迭代的 WeakMap 方法修复它。析构函数也可能存在问题二。
所有这些都会引发多层次的地狱,所以我建议尝试通过良好的程序设计、良好的实践、避免某些事情等来解决它。然而,它在 JS 中可能令人沮丧,因为它非常灵活在某些方面,因为它更自然地是异步的和基于事件的,具有严重的控制反转。
还有另一种解决方案相当优雅,但仍然存在一些潜在的严重问题。如果你有一个 class 扩展了一个 observable class 你可以覆盖事件函数。仅当事件是忠于自己。当您的所有事件都被删除后,再从 children 中删除您的事件。您还可以制作一个 class 来扩展您的可观察对象 class 来为您执行此操作。这样的 class 可以为空和 non-empty 提供挂钩,因为你会自己观察。这种方法不错,但也有问题。复杂性增加了,性能也降低了。您必须保留对您观察到的 object 的引用。至关重要的是,它也不适用于叶子,但如果你破坏叶子,至少中间体会自毁。这就像链接销毁但隐藏在您已经必须链接的调用后面。然而,一个很大的性能问题是,每次 class 激活时,您可能必须重新初始化来自 Observable 的内部数据。如果这个过程需要很长时间,那么你可能有麻烦了。
如果您可以迭代 WeakMap,那么您也许可以组合一些东西(当没有事件时切换到 Weak,当事件时切换到 Strong),但实际上所做的只是将性能问题推给其他人。
在行为方面,可迭代的 WeakMap 也有直接的烦恼。我之前简要提到过具有范围引用和雕刻的函数。如果我在构造函数中实例化一个 child 将侦听器 'console.log(param)' 挂钩到 parent 并且无法保留 parent 然后当我删除对 [=88= 的所有引用时] 它可以完全释放,因为添加到 parent 的匿名函数没有引用 child 中的任何内容。这就留下了如何处理 parent.weakmap.add(child, (param) => console.log(param)) 的问题。据我所知,密钥很弱但值不是,所以 weakmap.add(object, object) 是持久的。这是我需要重新评估的事情。对我来说,如果我处置所有其他 object 引用,这看起来像是内存泄漏,但我怀疑实际上它基本上通过将其视为循环引用来管理它。要么匿名函数维护对 objects 的隐式引用,这是由于 parent 范围的一致性浪费大量内存,或者您的行为会根据难以预测或管理的情况而变化。我觉得前者其实是不可能的。在后一种情况下,如果我在 class 上有一个方法,它只需要一个 object 并添加 console.log 它会在我清除对 class 的引用时被释放,即使我返回了函数并维护了一个引用。公平地说,这种特殊情况很少合法地需要,但最终有人会找到一个角度,并要求一个可迭代的 HalfWeakMap(释放键和值引用时免费),但这也是不可预测的(obj = null 神奇地结束 IO, f = null 神奇的结束 IO,两者都可以在令人难以置信的距离上实现)。
"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."
不是这样。析构函数的目的是允许注册侦听器的项目注销它们。一旦一个对象没有其他引用,它将被垃圾收集。
例如,在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() );
}
If there is no such mechanism, what is a pattern/convention for such problems?
术语 'cleanup' 可能更合适,但会使用 'destructor' 来匹配 OP
假设你写了一些 javascript 完全用 'function's 和 'var's。
然后你可以使用在try
/catch
/finally
格的框架内编写所有function
代码的模式。在 finally
内执行销毁代码。
而不是 C++ 风格的编写对象 classes 具有未指定的生命周期,然后通过任意范围指定生命周期并在范围结束时隐式调用 ~()
(~()
是 C++ 中的析构函数),在此 javascript 模式中,对象是函数,作用域恰好是函数作用域,析构函数是 finally
块。
如果您现在认为此模式存在固有缺陷,因为 try
/catch
/finally
不包含对 javascript 必不可少的异步执行,那么你是对的。幸运的是,自 2018 年以来,异步编程辅助对象 Promise
在已经存在的 resolve
和 catch
原型函数中添加了一个原型函数 finally
。这意味着需要析构函数的异步作用域可以用 Promise
对象编写,使用 finally
作为析构函数。此外,您可以在有或没有 await
的 async function
调用 Promise
中使用 try
/catch
/finally
,但必须注意 Promise
s 在没有等待的情况下调用将在范围外异步执行,因此在最终 then
.
中处理析构函数代码
在下面的代码中,PromiseA
和 PromiseB
是一些遗留的 API 级承诺,它们没有指定 finally
函数参数。 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中的每个对象都写成一个函数。相反,请考虑这样一种情况,您确定了一个范围,该范围实际上是 'wants' 一个在其生命周期结束时调用的析构函数。使用模式的 finally
块(或异步作用域中的 finally
函数)作为析构函数,将该作用域表述为函数对象。很可能制定该功能对象避免了对 non-function class 的需要,否则会编写 - 不需要额外的代码,对齐范围和 class 甚至可能更清晰.
注意:正如其他人所写,我们不应该混淆析构函数和垃圾收集。事实上,C++ 析构函数通常或主要与手动垃圾收集有关,但 不完全如此 。 Javascript 不需要手动垃圾收集,但异步作用域 end-of-life 通常是(取消)注册事件侦听器等的地方。
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()
})
其他答案已经详细解释了没有析构函数。但是您的实际目标似乎与事件相关。您有一个连接到某个事件的对象,并且您希望该连接在该对象被垃圾回收时自动消失。但这不会发生,因为事件订阅本身引用了监听函数。好吧,除非你使用这个漂亮的新 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)
单击 Subscribe 按钮会创建一个新的侦听器函数,并将其注册到 Emit 按钮的单击事件中。所以点击 Emit 按钮之后会向控制台打印一条消息。现在单击 Free 按钮,它只是将侦听器变量设置为 null,以便垃圾收集器可以删除侦听器。稍等片刻或在开发人员控制台中强制收集垃圾,然后再次单击 Emit 按钮。包装器侦听器函数现在看到实际的侦听器(包装在 WeakRef 中)不再存在,然后取消订阅按钮。
WeakRefs 非常强大,但请注意,无法保证您的内容是否以及何时被垃圾收集。
给你。 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 以了解不同的方法。
我知道 ECMAScript 6 有构造函数,但 ECMAScript 6 有析构函数吗?
例如,如果我在构造函数中将我对象的一些方法注册为事件侦听器,我想在我的对象被删除时删除它们。
一个解决方案是为每个需要这种行为的 class 创建一个 destructor
方法并手动调用它。这将删除对事件处理程序的引用,因此我的对象将真正准备好进行垃圾回收。否则它会因为这些方法而留在内存中。
但我希望 ECMAScript 6 是否有一些原生的东西可以在对象被垃圾回收之前调用。
如果没有这样的机制,对于这样的问题pattern/convention是什么?
Is there such a thing as destructors for ECMAScript 6?
没有。 EcmaScript 6 根本没有指定任何垃圾收集语义[1],所以也没有像 "destruction" 那样的东西。
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
析构函数在这里甚至帮不了你。事件侦听器本身仍然引用您的对象,因此在取消注册之前无法对其进行垃圾收集。
您实际上正在寻找的是一种注册侦听器而不将它们标记为活动根对象的方法。 (向您当地的事件源制造商咨询此类功能)。
1): 嗯,有一个开头的规范WeakMap
and WeakSet
objects. However, true weak references are still in the pipeline [1][2]。
我刚刚在搜索析构函数时遇到了这个问题,我认为您的评论中有部分问题没有得到解答,所以我想我会解决这个问题。
thank you guys. But what would be a good convention if ECMAScript doesn't have destructors? Should I create a method called destructor and call it manually when I'm done with the object? Any other idea?
如果你想告诉你的对象你现在已经完成了它并且它应该专门释放它拥有的任何事件侦听器,那么你可以创建一个普通的方法来做到这一点。您可以调用类似 release()
或 deregister()
或 unhook()
之类的方法。这个想法是你告诉对象将自己与它所连接的任何其他东西断开连接(注销事件监听器,清除外部对象引用等)。您将不得不在适当的时候手动调用它。
如果同时您还确保没有其他引用指向该对象,那么您的对象此时将符合垃圾回收条件。
ES6 确实有 weakMap 和 weakSet,它们是跟踪一组仍然存在的对象的方法,而不会影响它们何时可以被垃圾收集,但是当它们被垃圾收集时它不提供任何类型的通知。它们只是在某个时候(当它们被 GC 时)从 weakMap 或 weakSet 中消失。
仅供参考,您要求的这种类型的析构函数的问题(以及可能为什么没有太多要求)是因为垃圾收集,当一个项目有垃圾收集时,它不符合垃圾收集的条件针对活动对象的打开事件处理程序,因此即使有这样的析构函数,在您实际删除事件侦听器之前,它也永远不会在您的情况下被调用。而且,一旦您删除了事件侦听器,就不需要为此目的使用析构函数。
我想有可能 weakListener()
不会阻止垃圾收集,但这样的事情也不存在。
仅供参考,这是另一个相关问题 Why is the object destructor paradigm in garbage collected languages pervasively absent?。本次讨论涵盖终结器、析构器和处理器设计模式。我发现了解这三者之间的区别很有用。
2020 年编辑 - 对象终结器提案
有一个 Stage 3 EMCAScript proposal 可以在对象被垃圾回收后添加一个用户定义的终结器函数。
可以从此类功能中获益的典型示例是包含打开文件句柄的对象。如果对象被垃圾回收(因为没有其他代码仍然引用它),那么这个终结器方案允许至少向控制台发送一条消息,表明外部资源刚刚泄露,其他地方的代码应该被修复以防止这次泄漏。
如果您仔细阅读了该提案,您会发现它完全不像 C++ 等语言中成熟的析构函数。这个终结器在对象已经被销毁之后被调用,你必须预先确定实例数据的哪一部分需要传递给终结器来完成它的工作。此外,此功能并不是正常操作所依赖的,而是作为调试辅助工具和针对某些类型的错误的支持。您可以在提案中阅读对这些限制的完整解释。
您必须在 JS 中手动 "destruct" objects。创建销毁函数在 JS 中很常见。在其他语言中,这可能被称为 free、release、dispose、close 等。根据我的经验,尽管它往往会被销毁,这将解除内部引用、事件的挂钩,并可能传播对 child objects 的销毁调用还有。
WeakMaps 在很大程度上是无用的,因为它们不能被迭代,并且这可能要到 ECMA 7 才可用(如果有的话)。除了 object 引用和 GC 的查找之外,WeakMaps 让你做的就是拥有与 object 本身分离的不可见属性,这样它们就不会干扰它。这对于缓存、扩展和处理复数很有用,但它对可观察对象和观察者的内存管理没有真正帮助。 WeakSet 是 WeakMap 的一个子集(类似于默认值为布尔值 true 的 WeakMap)。
关于是否对 this 或析构函数使用弱引用的各种实现存在各种争论。两者都有潜在的问题,析构函数更受限制。
析构函数实际上对 observers/listeners 也可能无用,因为通常情况下,侦听器将直接或间接持有对观察者的引用。析构函数仅以没有弱引用的代理方式真正起作用。如果你的观察者真的只是一个代理,接受其他东西的听众并将它们放在一个可观察的对象上,那么它可以在那里做一些事情,但这种事情很少有用。析构函数更多的是用于IO相关的事情或者在包含范围之外做事情(IE,链接它创建的两个实例)。
我开始研究这个的具体情况是因为我有 class A 实例,它在构造函数中使用 class B,然后创建 class C 实例来监听B. 我总是把 B 实例放在高处。 A 我有时会扔掉,创建新的,创建很多,等等。在这种情况下,析构函数实际上对我有用,但如果我传递 C 实例但删除所有 A 引用,那么在 parent 中会产生令人讨厌的副作用那么C和B的绑定就会被破坏(C下面的地面被移除了)。
在 JS 中没有自动解决方案是痛苦的,但我认为这不是很容易解决的。考虑这些 classes(伪):
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.
});
}
附带说明一下,如果没有 anonymous/unique 稍后将介绍的功能,很难使事情正常进行。
在正常情况下实例化将是这样(伪):
var df = new Filter(stdin),
v1 = new View(df, stdout),
v2 = new View(df, stderr);
通常要对这些进行 GC,您会将它们设置为空,但它不会起作用,因为它们已经在根部创建了一个带有 stdin 的树。这基本上就是事件系统所做的。您将 parent 赋予 child,child 将自身添加到 parent,然后可能会或可能不会保留对 parent 的引用。树是一个简单的例子,但在现实中你也可能会发现自己有复杂的图形,尽管很少见。
在这种情况下,Filter 以匿名函数的形式将对自身的引用添加到 stdin,该匿名函数按作用域间接引用 Filter。范围引用是需要注意的,而且可能非常复杂。强大的 GC 可以做一些有趣的事情来剥离范围变量中的项目,但这是另一个话题。理解的关键是,当您创建一个匿名函数并将其作为 ab observable 的侦听器添加到某物时,observable 将维护对该函数的引用以及该函数在其上方范围内引用的任何内容(它在) 也将得到维护。视图做同样的事情,但是在执行它们的构造函数之后,children 不维护对它们的 parents 的引用。
如果我将上面声明的任何或所有变量设置为 null,它不会对任何事情产生影响(类似地,当它完成 "main" 范围时)。它们仍将处于活动状态并将数据从 stdin 传输到 stdout 和 stderr。
如果我将它们全部设置为 null,则在不清除 stdin 上的事件或将 stdin 设置为 null(假设它可以像这样释放)的情况下,不可能将它们删除或进行 GC。如果其余代码需要 stdin 并且上面有其他重要事件禁止您执行上述操作,那么您基本上会以这种方式发生内存泄漏,实际上是孤立的 objects。
为了摆脱 df、v1 和 v2,我需要对它们中的每一个调用 destroy 方法。在实现方面,这意味着 Filter 和 View 方法都需要保留对它们创建的匿名侦听器函数以及可观察对象的引用,并将其传递给 removeListener。
附带说明,或者你可以有一个可观察的 returns 一个索引来跟踪听众,这样你就可以添加原型函数,至少据我所知,这在性能和内存方面应该更好.您仍然必须跟踪返回的标识符并传递您的 object 以确保侦听器绑定到它在调用时。
销毁功能增加了一些麻烦。首先是我必须调用它并释放引用:
df.destroy();
v1.destroy();
v2.destroy();
df = v1 = v2 = null;
这是一个小麻烦,因为它的代码有点多,但这不是真正的问题。当我将这些参考资料交给许多 object 时。在这种情况下,你到底什么时候调用销毁?您不能简单地将这些交给其他 object。您最终会遇到一连串的破坏和通过程序流或其他方式手动实施跟踪。你不能一劳永逸。
这种问题的一个例子是,如果我决定 View 在销毁时也会在 df 上调用 destroy。如果 v2 仍然存在,则破坏 df 将破坏它,因此不能简单地将破坏传递给 df。相反,当 v1 使用 df 来使用它时,它需要告诉 df 它被使用,这会引发一些计数器或类似于 df。 df 的 destroy 函数会比 counter 减少,只有当它为 0 时才会真正销毁。这种事情增加了很多复杂性并增加了很多可能出错的地方,其中最明显的是销毁了一些东西,而在某个地方仍然有一个引用将使用和循环引用(此时不再是管理计数器的情况而是引用objects的映射)。当你想在 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);
};
在这种情况下,拥有者 class 还必须保留对 f 的标记引用,否则它会出错。
如果使用 Observable 而不是 EventListener,那么关于事件侦听器的内存管理将是自动的。
与其在每个 object 上调用销毁,这足以完全删除它们:
df = v1 = v2 = null;
如果您没有将 df 设置为 null,它仍然存在,但 v1 和 v2 会自动解除挂钩。
但是这种方法有两个问题。
问题一是它增加了新的复杂性。有时人们实际上并不想要这种行为。我可以创建一个非常大的 object 链,它们通过事件而不是包含(构造函数范围中的引用或 object 属性)相互链接。最终,一棵树和我只需要绕过根部并为此担心。释放根将方便地释放整个东西。取决于编码风格等的两种行为都很有用,并且在创建可重用的 objects 时,很难知道人们想要什么,他们做了什么,你做了什么,并且很难解决已经发生的事情完毕。如果我使用 Observable 而不是 EventListener,那么 df 将需要引用 v1 和 v2,或者如果我想将引用的所有权转移到范围之外的其他对象,我将不得不全部传递它们。类似弱引用的东西可以通过将控制权从 Observable 转移到观察者来稍微缓解这个问题,但不会完全解决它(并且需要检查每个发射或事件本身)。这个问题可以解决我想如果行为只适用于孤立的图,这会使 GC 严重复杂化,并且不适用于图外有引用的情况,这些引用实际上是 noops(只消耗 CPU 周期,不进行了更改)。
问题二是在某些情况下它是不可预测的,或者强制 JS 引擎根据需要遍历那些 objects 的 GC 图,这可能会产生可怕的性能影响(尽管如果它很聪明它可以通过每个 WeakMap 循环来避免对每个成员进行操作)。如果内存使用未达到特定阈值,GC 可能永远不会 运行,并且 object 及其事件不会被删除。如果我将 v1 设置为 null,它可能仍会永远中继到标准输出。即使它确实得到了 GC,这也是任意的,它可能会继续中继到标准输出任何时间(1 行、10 行、2.5 行等)。
当 non-iterable 时 WeakMap 不关心 GC 的原因是要访问 object 无论如何你必须有一个对它的引用所以它没有被 GC 或尚未添加到地图中。
我不确定我对这种事情的看法。您有点破坏内存管理以使用可迭代的 WeakMap 方法修复它。析构函数也可能存在问题二。
所有这些都会引发多层次的地狱,所以我建议尝试通过良好的程序设计、良好的实践、避免某些事情等来解决它。然而,它在 JS 中可能令人沮丧,因为它非常灵活在某些方面,因为它更自然地是异步的和基于事件的,具有严重的控制反转。
还有另一种解决方案相当优雅,但仍然存在一些潜在的严重问题。如果你有一个 class 扩展了一个 observable class 你可以覆盖事件函数。仅当事件是忠于自己。当您的所有事件都被删除后,再从 children 中删除您的事件。您还可以制作一个 class 来扩展您的可观察对象 class 来为您执行此操作。这样的 class 可以为空和 non-empty 提供挂钩,因为你会自己观察。这种方法不错,但也有问题。复杂性增加了,性能也降低了。您必须保留对您观察到的 object 的引用。至关重要的是,它也不适用于叶子,但如果你破坏叶子,至少中间体会自毁。这就像链接销毁但隐藏在您已经必须链接的调用后面。然而,一个很大的性能问题是,每次 class 激活时,您可能必须重新初始化来自 Observable 的内部数据。如果这个过程需要很长时间,那么你可能有麻烦了。
如果您可以迭代 WeakMap,那么您也许可以组合一些东西(当没有事件时切换到 Weak,当事件时切换到 Strong),但实际上所做的只是将性能问题推给其他人。
在行为方面,可迭代的 WeakMap 也有直接的烦恼。我之前简要提到过具有范围引用和雕刻的函数。如果我在构造函数中实例化一个 child 将侦听器 'console.log(param)' 挂钩到 parent 并且无法保留 parent 然后当我删除对 [=88= 的所有引用时] 它可以完全释放,因为添加到 parent 的匿名函数没有引用 child 中的任何内容。这就留下了如何处理 parent.weakmap.add(child, (param) => console.log(param)) 的问题。据我所知,密钥很弱但值不是,所以 weakmap.add(object, object) 是持久的。这是我需要重新评估的事情。对我来说,如果我处置所有其他 object 引用,这看起来像是内存泄漏,但我怀疑实际上它基本上通过将其视为循环引用来管理它。要么匿名函数维护对 objects 的隐式引用,这是由于 parent 范围的一致性浪费大量内存,或者您的行为会根据难以预测或管理的情况而变化。我觉得前者其实是不可能的。在后一种情况下,如果我在 class 上有一个方法,它只需要一个 object 并添加 console.log 它会在我清除对 class 的引用时被释放,即使我返回了函数并维护了一个引用。公平地说,这种特殊情况很少合法地需要,但最终有人会找到一个角度,并要求一个可迭代的 HalfWeakMap(释放键和值引用时免费),但这也是不可预测的(obj = null 神奇地结束 IO, f = null 神奇的结束 IO,两者都可以在令人难以置信的距离上实现)。
"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."
不是这样。析构函数的目的是允许注册侦听器的项目注销它们。一旦一个对象没有其他引用,它将被垃圾收集。
例如,在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() );
}
If there is no such mechanism, what is a pattern/convention for such problems?
术语 'cleanup' 可能更合适,但会使用 'destructor' 来匹配 OP
假设你写了一些 javascript 完全用 'function's 和 'var's。
然后你可以使用在try
/catch
/finally
格的框架内编写所有function
代码的模式。在 finally
内执行销毁代码。
而不是 C++ 风格的编写对象 classes 具有未指定的生命周期,然后通过任意范围指定生命周期并在范围结束时隐式调用 ~()
(~()
是 C++ 中的析构函数),在此 javascript 模式中,对象是函数,作用域恰好是函数作用域,析构函数是 finally
块。
如果您现在认为此模式存在固有缺陷,因为 try
/catch
/finally
不包含对 javascript 必不可少的异步执行,那么你是对的。幸运的是,自 2018 年以来,异步编程辅助对象 Promise
在已经存在的 resolve
和 catch
原型函数中添加了一个原型函数 finally
。这意味着需要析构函数的异步作用域可以用 Promise
对象编写,使用 finally
作为析构函数。此外,您可以在有或没有 await
的 async function
调用 Promise
中使用 try
/catch
/finally
,但必须注意 Promise
s 在没有等待的情况下调用将在范围外异步执行,因此在最终 then
.
在下面的代码中,PromiseA
和 PromiseB
是一些遗留的 API 级承诺,它们没有指定 finally
函数参数。 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中的每个对象都写成一个函数。相反,请考虑这样一种情况,您确定了一个范围,该范围实际上是 'wants' 一个在其生命周期结束时调用的析构函数。使用模式的 finally
块(或异步作用域中的 finally
函数)作为析构函数,将该作用域表述为函数对象。很可能制定该功能对象避免了对 non-function class 的需要,否则会编写 - 不需要额外的代码,对齐范围和 class 甚至可能更清晰.
注意:正如其他人所写,我们不应该混淆析构函数和垃圾收集。事实上,C++ 析构函数通常或主要与手动垃圾收集有关,但 不完全如此 。 Javascript 不需要手动垃圾收集,但异步作用域 end-of-life 通常是(取消)注册事件侦听器等的地方。
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()
})
其他答案已经详细解释了没有析构函数。但是您的实际目标似乎与事件相关。您有一个连接到某个事件的对象,并且您希望该连接在该对象被垃圾回收时自动消失。但这不会发生,因为事件订阅本身引用了监听函数。好吧,除非你使用这个漂亮的新 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)
单击 Subscribe 按钮会创建一个新的侦听器函数,并将其注册到 Emit 按钮的单击事件中。所以点击 Emit 按钮之后会向控制台打印一条消息。现在单击 Free 按钮,它只是将侦听器变量设置为 null,以便垃圾收集器可以删除侦听器。稍等片刻或在开发人员控制台中强制收集垃圾,然后再次单击 Emit 按钮。包装器侦听器函数现在看到实际的侦听器(包装在 WeakRef 中)不再存在,然后取消订阅按钮。
WeakRefs 非常强大,但请注意,无法保证您的内容是否以及何时被垃圾收集。
给你。 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 以了解不同的方法。