Why are WeakRef polyfills made with WeakMap? I don't see how that can work

827 views Asked by At

If you look at both of these examples of WeakRef polyfills, they both use WeakMap.

But I don't see how that can work. A WeakMap doesn't hold weak references to its values, but to its keys. And both those polyfills use this as the key. Which means if I say let weakRef = new WeakRef(targetObject), then targetObject will never get garbage collected unless I throw away weakRef. Which negates the entire purpose of WeakRef, doesn't it?

In my limited experimentation my theory seems to be correct. Check out this jsfiddle.

Furthermore, does anyone know a WeakRef polyfill that does work?

2

There are 2 answers

1
The Vee On

It seems this is a result of misunderstanding on part of the polyfill authors, please consider writing a bug report.

These approaches indeed keep a strong reference to the object, an implementation which is technically conforming, because the program can't argue that a garbage collector is broken just because it was never observed removing an object. However, to achieve this feat would have been much easier: just store the object in a property and don't bother with a WeakMap at all.

I think some people naïvely expect that a WeakMap has weak references to the values, as you observed. I also used to have such misconception before reading the docs carefully. Nevertheless, this would not explain why WeakMap has been in the spec since 2015 while WeakRef hasn't made it past the proposal stage yet, for over 3 years. Such naïve collection would be like a Map of WeakRefs, so why would the language not also expose a single one?

Truth is, it is impossible to mimic WeakRef with WeakMap and careful design has been taken to make it so (primarily, that it is not enumerable). Why? While it is perfectly possible to implement this behaviour reliably, the sentiment in the design group is towards determinism and predictability. This short paragraph summarizes it nicely, with reasoning and an example considered a wrong design past decision. The following two sentences directly preclude anything like a WeakRef:

This means that you shouldn’t expose any API that acts as a weak reference, e.g. with a property that becomes null once garbage collection runs. Object and data lifetimes in JavaScript code should be predictable.

With this in mind, in an implementation following the W3C TAG guidelines, not only WeakMap can't be used, but not even any other approach (which should answer your last question in the comments), except possibly using an ugly and unreliable loophole like that mentioned in the example, for narrowly specific cases. Unless the opinion of the committee changes radically, the WeakRef proposal may never really be standardized. Let's hope that a carefully worded note of caution, as included in the current version, and an appeal to excess memory usage and other interesting and well-founded use cases, will eventually allow such exception.

0
user3840170 On

You are correct in that both polyfills’ implementation of WeakRef holds a strong reference to the target. A WeakMap holds a strong reference to a value as long as someone else holds a strong reference to its key; since the key is the WeakRef, this means holding a WeakRef strongly will prevent garbage collection of its referent. Such an implementation is technically correct, the best kind of correct: this is because the no-op garbage collector is a perfectly valid garbage collector.

The only reason to use WeakMap is not for any memory reclamation effects, but for the sake of encapsulation: to ensure the only way a holder of a WeakRef can obtain its referent is by calling WeakRef.prototype.deref, and cannot access it otherwise; not even by guessing a property name. An equivalent implementation based on ECMAScript 2021 private field syntax would be as follows:

class WeakRef {
    #target;
    constructor(target) {
        this.#target = target;
    }
    deref() {
        return this.#target;
    }
}

However, the point of a polyfill is to be portable to older implementations of ECMAScript, which usually fail to provide the latest syntax. An alternative would be to make the deref method a closure held directly in the object instead of WeakRef.prototype, but this would make the implementation observably diverge from the specification.


Now, if you want an implementation of WeakRef that occasionally clears weak references, take a look at Mathieu Hofman’s shim; it is based on an earlier version of the spec, which is most noticeable with the naming of FinalizationGroup. That shim manages to implement a non-degenerate WeakRef in a number of environments, using host-specific APIs for each case. You can forget about applying it in a browser, though, since browsers will not expose such APIs. WeakRef is not polyfillable in general: the API (or something equivalent in power) must be exposed by the host as a primitive, or it cannot be implemented at all.