Question

Since I've dealt in the past with JavaScript’s funky "object model", I assume there is no such thing as a destructor. My searches were mildly unsuccessful, so you guys are my last hope. How do you execute stuff upon instance destruction?

Was it helpful?

Solution

MDN is a nice resource for JS. No, there is nothing like calling a function when an object ceases.

OTHER TIPS

FinalizationRegistry might be what you need. It is not a destructor, but it executes a function once the object is garbage collected. In any case, this is what I wish I had find when I first came on here :)

Aside from the already-mentioned FinalizationRegistry that provides support for non-deterministic finalization, there is (as of this writing, March 2023) a proposal in the works adding a deterministic, scope-based cleanup construct to the language: Explicit Resource Management. It may land in the standard in 2023 or perhaps 2024.

The syntax has undergone a number of iterations, but given that it’s already past stage-3 review, it’s unlikely to change much now. Here’s a demonstration wrapping URL.createObjectURL:

class BlobURL {
    #url = null;
    constructor(blob) {
        this.#url = URL.createObjectURL(blob);
    }
    toString() {
        if (this.#url == null)
            throw new TypeError("URL was already revoked");
        return this.#url;
    }
    valueOf() {
        return this.#url;
    }
    [Symbol.dispose]() {
        URL.revokeObjectURL(this.#url);
        this.#url = null;
    }
}

const doIFeelLucky = (url) => {
    // imagine some process here that
    // uses the URL, but may throw

    if (6 * Math.random() < 1)
        throw new Error("seems I don't");
};

const processBlob = (blob) => {
    using url = new BlobURL(blob);
    doIFeelLucky(url);
    // `url` is revoked when the function returns or throws
};

Ownership transfer is a bit clunky, as it requires creating a DisposableStack to manage it:

const processBlobAndReturnItsURL = (blob) => {
    using stack = new DisposableStack();
    const url = stack.use(new BlobURL(blob));
    doIFeelLucky(url);   // if this throws, `url` will be revoked
    stack.move();        // release ownership
    return url;          // `url` will be returned without being revoked
}

Other downsides: there is no support for destructuring (despite numerous suggestions made to allow it while the proposal proceeded), and the construct is not expressive enough to perform exception-catching like Python context managers (though whether that would actually make sense is debatable). Still, it’s marginally better than finally, which is what we have now.

As of more recently, this link is of more use to answer this question. The most important part:

As of 2012, all modern browsers ship a mark-and-sweep garbage-collector.

...

Cycles are no longer a problem

In the first example above, after the function call returns, the two objects are no longer referenced by any resource that is reachable from the global object. Consequently, they will be found unreachable by the garbage collector and have their allocated memory reclaimed.

Limitation: Releasing memory manually

There are times when it would be convenient to manually decide when and what memory is released. In order to release the memory of an object, it needs to be made explicitly unreachable.

So as far as cyclic references goes, de[con]structors aren't really needed.


One cool trick I have thought of though, if you have cyclic references and you want easy manual control over deconstruction...

class Container {
  constructor() {
    this.thisRef = [ this ];
    this.containee = new Containee({ containerRef: this.thisRef });
  }

  //Note: deconstructor is not an actual JS thing/keyword.
  deconstructor() {
    //Have to delete `this.thisRef[0]` and not `this.thisRef`, in
    //order to ensure Containee's reference to Container is removed.
    delete this.thisRef[0];
  }

  doSomething() {

  }
}

class Containee {
  constructor({ containerRef }) {
    //Assumption here is, if the Container is destroyed, so will the Containee be
    //destroyed. No need to delete containerRef, no need for a
    //deconstructor function!
    this.containerRef = containerRef;
  }

  someFunc() {
    this.containerRef[0].doSomething();
  }
}

let c = new Container();
...
//No cyclic references!
c.deconstructor();

So here, instead of the Containee class storing a direct reference to the Container instance, it stores a reference to a size 1 array containing the Container reference, which the Container instance itself can then delete itself from. The array, with the reference, is managed by Container.

But again, this isn't really needed, since garbage collection in all modern browsers is mark-and-sweep and can handle cyclic references.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top