skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    I came up with this with hints from @Bergi:

    const finalizationRegistry = new FinalizationRegistry(interval => {
        clearInterval(interval);
        console.log('destructor()');
    });
    
    class Thing {
        #intervalId;
        constructor() {
            console.log('constructor()');
            let count = 0;
            const ref = new WeakRef(this);
            this.#intervalId = setInterval(() => {
                const self = ref.deref();
                if (self) {
                    self.log(`test ${count++}`);
                }
            }, 1000);
            finalizationRegistry.register(this, this.#intervalId);
        }
        log(message) {
            console.log(message);
        }
    }
    
    (() => {
        const x = new Thing();
    })();
    
    
    if (global.gc) {
        setTimeout(() => {
            global.gc();
        }, 5000);
    } else {
        console.log('Garbage collection unavailable.  Pass --expose-gc '
          + 'when launching node to enable forced garbage collection.');
    }
    

    I used WeakRef since I needed to call the #prune method (as stated in the question) since without this kept inside the function and the object was never garbage collected.

    It works in NodeJS when called with node --expose-gc script.js.

    Note that I think that real destructor is not possible and you need to have the code that clean up the class like clear of interval outside of the class.


  2. This is not how you use a FinalizationRegistry.

    First off, note that JS doesn’t have proper destructors, FinalizationRegistry may run callbacks a lot later than you’d expect (or might never run them).

    Second, the code needs several changes to work (see comments):

    class Thing{
        constructor() {
            console.log('constructor()');
            let count = 0;
            const intervalId = setInterval(() => {
                console.log(`Test ${count++}`);
            }, 1000);
           
           // Pass `intervalId` as a "testament": that's the only value the destructor will remember. You can pass an object if you'd need to pass multiple values
           Thing.#registry.register(this, intervalId);
        }
        
        // Create one single registry, not one for each instance of `Thing`
        static #registry = new FinalizationRegistry(Thing.#destructor);
        // Make destructor `static`, because it can't access `this` -- the object is already destroyed when it is called
        static #destructor(testament){
            console.log('destructor()');
            clearInterval(testament);
        }
    }
    
    {
       new Thing();
    }

    A bit of advice (from own experience): don’t use ChatGPT to generate code, use it only as a thought starter and research ideas yourself. That is harder, but otherwise you’ll likely end up with some code that you don’t understand and which may be indefinitely wrong.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search