Search code examples
javascriptgarbage-collection

How to use FinalizationRegistry to clear interval inside a class constructor when object is destroyed?


I have this code created with help from ChatGPT:

class Thing {
    #intervalId;
    #finalizationRegistry;
    constructor() {
        console.log('constructor()');
        let count = 0;
        this.#intervalId = setInterval(() => {
            console.log(`Test ${count++}`);
        }, 1000);

        this.#finalizationRegistry = new FinalizationRegistry(() => {
            this.#destructor();
        });
        this.#finalizationRegistry.register(this);
    }
    #destructor() {
        console.log('destructor()');
        clearInterval(this.#intervalId);
    }
}

(() => {
    const x = new Thing();
})();

This is my attempt to create a destructor for a class in JavaScript. I'm not sure if it works because the console.log('destructor()'); was not called when I tested. My first code used a dummy object inside the constructor as a help value and that was working, it was triggering when I assigned x = null but the code was wrong.

The above code is just the smallest possible example of clearing setInterval with FinalizationRegistry but I'm not sure if it works.

Is there a way to test that this actually works and call destructor?

My real question is that I want to implement Cache class and I need to trigger this function inside interval:

this.#intervalId = setInterval(() => {
   this.#prune();
}, 1000);

And I want to clear the timer when this is not accessible anymore. Like with the above code:

(() => {
    const x = new Thing();
})();

Can you create an interval inside the class constructor that references this and clear the interval when the object is garbage collected? Can the object even be garbage collected when intervals keep a reference to this? If not then is there a workaround?


Solution

  • (I originally failed to address OP's second point, and now I rewrote the answer. See the original in the edit history.)

    To access a method from the inverval, you may not use this directly, because that would keep the object alive, and the destructor would never run.

    Instead, you have to use a WeakRef to this. That way, you don't even need a FinalizationRegistry, because you can clear the interval from the interval itself when the object is dropped.

    class Thing {
        constructor() {
            console.log('constructor()');
    
            let count = 0;
            const weakRef = new WeakRef(this);
    
            //Using a `function` to explicitly prevent access to `this`:
            const intervalId = setInterval(function (){
                console.log(`Test ${count++}`);
    
                const self = weakRef.deref();
    
                if(self) {
                    self.#prune();
                } else {
                    console.log('clearInterval()');
                    clearInterval(intervalId);
                }
            }, 1000);
        }
        #prune(){
            console.log('prune()');
        }
    }
    
    new Thing();