ECMAScript 6类析构函数

时间:2015-03-29 18:22:41

标签: javascript ecmascript-6

我知道ECMAScript 6有构造函数但是ECMAScript 6的析构函数是否有这样的东西?

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

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

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

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

7 个答案:

答案 0 :(得分:20)

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

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

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

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

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

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

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

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

答案 1 :(得分:16)

  

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

没有。 EcmaScript 6并没有在所有 [1] 中指定任何垃圾收集语义,所以没有像" destroy"任

  

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

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

1):嗯,WeakMapWeakSet对象的规范有一个开头。但是,真正的弱引用仍然在管道中[1] [2]

答案 2 :(得分:8)

你必须手动"破坏" JS中的对象。在JS中创建销毁函数很常见。在其他语言中,这可能被称为free,release,dispose,close等。根据我的经验,虽然它往往会被破坏,它会取消内部引用,事件和可能的传播也会破坏对子对象的调用。

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

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

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

var df = new Filter(stdin),
    v1 = new View(df, stdout),
    v2 = new View(df, stderr);

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

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

如果我将上面声明的任何或所有变量设置为null,则它不会对任何事物产生影响(类似于它完成了#34; main"范围)。它们仍然是活动的,并将数据从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;

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

这种问题的一个例子是,如果我确定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);
};

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

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

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

df = v1 = v2 = null;

如果你没有将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,两者都能在令人难以置信的距离内完成。)

答案 3 :(得分:0)

  

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

不是这样。析构函数的目的是允许注册侦听器的项目取消注册它们。例如,在Angular中,当控制器被销毁时,它可以侦听销毁事件并对其进行响应。这与自动调用析构函数不同,但它很接近,并且让我们有机会删除控制器初始化时设置的侦听器。

// Initialize the controller
        function initialize() {

            // Set event listeners, hanging onto the returned listener removal functions
            $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 ) );
            $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.RESET_PW.SUCCESS, instance.onSendResetEmailResponse ) );
            $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.RESET_PW.FAILURE, instance.onSendResetEmailResponse ) );
            $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.CHANGE_PW.SUCCESS, instance.onChangePasswordResponse ) );
            $scope.listenerCleanup.push( $scope.$on( AUTH_SERVICE_RESPONSES.CHANGE_PW.FAILURE, instance.onChangePasswordResponse ) );
            $scope.listenerCleanup.push( $scope.$on( ACCOUNT_SERVICE_RESPONSES.CREATE_PROFILE.SUCCESS, instance.onCreateProfileResponse ) );
            $scope.listenerCleanup.push( $scope.$on( ACCOUNT_SERVICE_RESPONSES.CREATE_PROFILE.FAILURE, instance.onCreateProfileResponse ) );
            };
        }

        /**
         * Remove event listeners when the controller is destroyed
         */
        function onDestroy(){
            var i, removeListener;
            for (i=0; i < $scope.listenerCleanup.length; i++){
                removeListener = $scope.listenerCleanup[i];
                removeListener();
            }
        }

答案 4 :(得分:0)

  

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

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

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

代替C ++风格编写具有未指定生存期的对象类,然后通过任意作用域指定生存期,并在作用域末尾对~()进行隐式调用(~()在C ++中是析构函数),此javascript模式的对象是函数,作用域恰好是函数作用域,而析构函数是finally块。

如果您现在认为此模式存在缺陷,因为try / catch / finally不包含对javascript至关重要的异步执行,那么您是正确的。幸运的是,自2018年以来,异步编程帮助器对象Promise的原型函数finally已添加到已经存在的resolvecatch原型函数中。这意味着可以使用Promise作为析构函数,使用finally对象编写需要析构函数的异步作用域。此外,您可以在try中使用catch / finally / async function在调用Promise且有或没有await的情况下使用Promise,但必须注意{无需等待就调用的{1}}将在范围之外异步执行,因此将在最终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不需要手动进行垃圾回收,但是异步作用域终止通常是(取消)注册事件侦听器等的地方。

答案 5 :(得分:0)

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

答案 6 :(得分:0)

其他答案已经详细解释了没有析构函数。但是您的实际目标似乎与事件有关。您有一个连接到某个事件的对象,并且您希望该连接在对象被垃圾收集时自动消失。但这不会发生,因为事件订阅本身引用了侦听器函数。好吧,除非你使用这个漂亮的新 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,以便垃圾收集器可以删除侦听器。等待一段时间或在开发者控制台中强制收集 gargabe,然后再次单击 Emit 按钮。包装侦听器函数现在看到实际侦听器(包装在 WeakRef 中)不再存在,然后从按钮取消订阅。

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