# Built-in function memoization with weak references

**URL:** <https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446>\
**Category:** 💡 Ideas\
**Tags:** proposal\
**Created:** [August 28, 2020, 5:22pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446 "2020-08-28T17:22:55Z")\
**Posts on this page:** 18\
**Page:** 1

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [August 28, 2020, 5:22pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/1 "2020-08-28T17:22:56Z")

</div>

Function memoization is a useful feature in writing performant applications, but there are multiple common implementations that each differ in their usefulness and shortcomings.

Lodash uses only the first argument by default, which can cause collisions in retrieving the correct result. I've seen one implementation that used `JSON.stringify` on the arguments object which can fail if any argument isn't stringifiable, and stringifying can be as slow as whatever the function was that was being memoized. Recently, I used an in-house memoization function that relied on `WeakMap` to allow the arguments to be garbage collected, but nesting these calls accidentally created a strong reference, nullifying the benefit. The WeakMap feature also meant that it couldn't memoize primitive arguments since these can't be keys in a WeakMap.

```javascript
weakMemo(thing1 => 
  weakMemo(thing2 => 
    // thing1 is strongly referenced here
    doStuff(thing1, thing2)
  )
)

```

I'd like to propose a language-level memoization feature that correctly weakly references non-primitives, and compares the entire argument list. I think this comparison could be a plain strict equality (===) check or it could be the result of a resolver function provided to the memoization call like what Lodash allows. ([https://lodash.com/docs/#memoize](https://lodash.com/docs/#memoize)).

Since this would be implemented at the JS engine level, implementations could differ on how/when/if the function gets memoized or its lookup mechanism gets cleared. In space-constrained environments like IOT devices, the call to memoize a function could be ignored. In environments where space is at first plentiful, but gets filled up, the call to memoize a function could work as expected while space is available, then stop using the memoization mechanism as space is constrained. This would effectively be a JS engine hint from the programmer that some function is time-intensive, and the engine can/should use a memoization strategy (but not at the expense of keeping strong references).

Proposed syntax:

```javascript
Function.memoize(myFunc);

```

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [September 7, 2020, 6:58am UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/2 "2020-09-07T06:58:19Z")

</div>

What about primitives? Those necessarily must use strong references. Also, keep in mind, _any_ sane implementation would only weakly reference objects and symbols where possible, regardless of what the spec says, as it saves a _ton_ on memory overhead.

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 8, 2020, 3:54pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/3 "2020-09-08T15:54:06Z")

</div>

Primitives wouldn't be weakly referenced, but they also do not have as much potential for memory leaks as objects do. Here's what I envision:

```javascript
// declare function
function myFunc(object, array, string, boolean, symbol) {
  /* expensive computations */
}

// memoize
Function.memoize(myFunc);

const context = [
  {}, [], 'hello world', Symbol()
];

// initial invocation
myFunc(...context);

// memoized invocation
myFunc(...context);

// remove the object parameter
delete context[0];

// when GC (garbage collection) runs next, 
// it will remove the object from memory,
// ignoring the reference in the memoized myFunc's cache

```

In the above example, there is no way to _call_ `myFunc` again with the arguments that it has memoized. What this proposal is mostly concerned about is ensuring that the memoization mechanism doesn't have a memory leak. The only implementations I've seen that try to address that fail, and I'm not sure if there is a way to write a memoization method in JS that doesn't leak memory. Because of this, I'm convinced that the only way this will happen is with a built-in mechanism that works at a lower level than JS.

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [September 15, 2020, 7:19am UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/4 "2020-09-15T07:19:23Z")

</div>

Strings. There's your main source of memory leaks with primitives. You call it with a string of file contents, that right there is an easy way to ramp up memory usage really quick.

Also, `Function.memoize` should _not_ be mutating functions. Engines rely on the fact functions are never modifiable after creation to simplify their optimization pipeline tremendously - it also allows them to do things like optimize them off-thread while they're being used, and just phase back into it once it is.

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 15, 2020, 4:52pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/5 "2020-09-15T16:52:48Z")

</div>

That's a great point (about not mutating functions). Perhaps, this would be better:

```javascript
const memoizedFunc = Function.memoize(myFunc);

```

Or we could introduce a syntax that would signify the function should be memoized at the time it's declared/created:

```javascript
function memoizeMe #(a, b, c) { /*...*/ }

```

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 17, 2020, 4:25am UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/6 "2020-09-17T04:25:56Z")

</div>

After further consideration, I dislike the syntax route. We don't always know at the time a function is written if it should be memoized. For example:

```javascript
// All good
const fastFn = createFn(
  these,
  arguments,
  make,
  a,
  FAST,
  func
);

// Uh-oh
const slowFn = createFn(
  these,
  arguments,
  make,
  a,
  SLOW,
  func
);

// All better now
const alsoFastFn = Function.memoize(slowFn);

```

I like that this doesn't introduce new syntax and only adds to the `Function` namespace. For cases when the JavaScript engine decides not to memoize the function, the return value could be the same as the return value of `Function.prototype.bind.call(fn, null)`.

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 18, 2020, 2:54am UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/7 "2020-09-18T02:54:52Z")

</div>

I think we'll need the hashing function as the optional second parameter. This would handle the case where deeply equal but not referentially equal object arguments give identical return values. The signature would be:

## Syntax

```javascript
Function.memoize(fn [, hashingFn])

```

## Parameters

```javascript
fn

```

The function to be memoized.

```javascript
hashingFn

```

A function to generate a string that will be used as a key for the memoization cache. This function receives the arguments passed to the memoized function and must return a string. Any other return type is ignored and the arguments themselves are used as the cache key (as would happen if no hashing function were passed).

## Return value

A copy of the original function that is either memoized or the equivalent of `Function.prototype.bind.call(fn, null)`. Exactly which is returned is determined by the JavaScript engine. Note that the memoized function may be deoptimized at any time according to whatever heuristics the JavaScript engine uses to determine if space is constrained.

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [September 29, 2020, 8:15pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/8 "2020-09-29T20:15:16Z")

</div>

I honestly wouldn't mind a built-in function for this, provided the memoization can be configured and provided the cache itself is not enumerable (allowing objects to be referenced weakly). Just to name a couple:

- React needs to memoize not by raw argument but by object key/value.
- Long-running servers and in-memory network caches will want to put a time limit for how long values exist in cache, and some may also want a way to clear the cache entirely.

My biggest use case for this personally is related to the second: memoizing async actions correctly without having to use an intermediate function (either named or IIAFE) just to keep using async/await.

Conceptually, I could see an API like this work:

```javascript
// Never evicted, caches based on arguments
const memoized = func.memoize()

const customMemoized = func.memoize({
  // Evict after this promise resolves - can be controlled by arguments
  // as well.
  evictAfter: (...args) =>
    new Promise(resolve => setTimeout(resolve, 5000)),

  // Get the arguments to use for caching - can return any iterable,
  // including arrays and generators.
  select: function *selector(...args) {
    // What React might do
    const [props, ...rest] = args
    yield* rest
    yield* Object.keys(props)
    yield* Object.values(props)
  },
})

// Defaults, but explicit.
const defaultMemoized = func.memoize({
  evictAfter: () => new Promise(() => {}), // never resolved
  select(...args) { return [this, ..args] },
})

```

Why I went with that:

- Relying on promises, iterables, and raw arguments lists makes it fairly intuitive, and for simple cases like single parameters, also fairly easy.
- `evictAfter` does _not_ require any new host hooks to implement.
- Having `select` be iterable-based makes it much more efficient for cases where the key path is much more complicated. Plus, each `yield` maps very directly to an underlying cache access, so it's also fairly low-level.
- Passing arguments to `evictAfter` allows much more complicated logic like:
  - Invalidating it on web socket message, if you use an intermediate key → `resolve` map
  - Allowing callers to have control over cache times
  - Tracking a list of active cache entries via an intermediate `resolve` set and simply invoking them all
  - Accepting weak references and memoizing based on that
  - Memoizing based on a subset of parameters

Here's a rough polyfill sketch of that (minus the needed weak references for objects):

```javascript
Function.prototype.memoize = function (opts) {
  const func = this

  if (typeof func !== "function") {
    throw new TypeError('this must be callable')
  }

  let select, evictAfter
  if (opts != null) {
    select = opts.select
    if (select != null && typeof select !== 'function') {
      throw new TypeError('opts.select must be callable if present')
    }

    evictAfter = opts.evictAfter
    if (evictAfter != null && typeof evictAfter !== 'function') {
      throw new TypeError('opts.evictAfter must be callable if present')
    }
  }

  const hole = {}
  function create(parent, key) {
    return {parent, key, map: new Map(), value: hole}
  }

  const rootCache = create(undefined, undefined)

  function get(parent, key) {
    let child = parent.map.get(key)
    if (child == null) {
      child = create(parent, key)
      parent.map.set(key, child)
    }
    return child
  }

  return function () {
    const cachePath = select != null
      ? [...select.apply(this, arguments)]
      : [this, ...arguments]

    let cache = rootCache
    for (const key of cachePath) {
      cache = get(cache, key)
    }

    if (cache.value !== hole) {
      return cache.value
    }

    // Executed before the actual function so things like timeouts are
    // scheduled accurately
    if (evict != null) {
      (async () => {
        await evict.apply(this, arguments)
        let current = cache
        current.value = hole
        // Collect garbage
        while (current !== rootCache && !current.map.size) {
          current.parent.map.delete(current.key)
          current = current.parent
        }
      })()
    }

    return cache.value = func.apply(this, arguments)
  }
}

```

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [October 2, 2020, 8:56pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/9 "2020-10-02T20:56:59Z")

</div>

I think that the memoization API would benefit from being as simple as possible, even if it means not covering every use case. Having a `select` function as a way to get the cache keys could cause problems with the weak referencing I hope to achieve. The author could accidentally keep a reference to the arguments outside of the function. This is also a potential problem with the hashing function I proposed 🤔.

The React use case (memoizing based on object keys and values) could be done without a `select` function:

```javascript
// the first half of the array is the keys, the second is the values
// zip them into entries, then convert to an object
const keysAndValuesToObj = (...kv) => Object.fromEntries(
  _.zip(
    kv.slice(0, kv.length / 2),
    kv.slice(kv.length / 2, kv.length)
  )
);

React.memo = fn => {
  // spread out the keys and values for the memoized function,
  // then gather them back into an object for the React function call
  const _fn = Function.memoize((...kv) => fn(keysAndValuesToObj(kv)));

  // now the function will be memoized based on keys and values
  return props => _fn(...Object.keys(props), ...Object.values(props));
}

```

I like the idea of cache-busting, but it should also be a **hint** to the JS engine that the cache is no longer needed. The whole caching/memoizing process should be transparent to the JS author. The author should have no access to the cache directly. I originally considered something like this:

```javascript
const memoFn = Function.memoize(fn);
// ...
memoFn.clearCache();

```

This has the unfortunate knock-on effect of checking if `clearCache` exists on any given function (and is callable) before invoking. I think an API similar to `setTimeout` and `clearTimeout` would be more robust:

```javascript
const memoFn = Function.memoize(fn);
//...
Function.dememoize(memoFn);

```

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [October 3, 2020, 12:35am UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/10 "2020-10-03T00:35:25Z")

</div>

> [@NickGard](#):
>
> The React use case (memoizing based on object keys and values) could be done without a `select` function:

This is true, and I briefly considered it, but that path is very perf-critical, and yes, even so much as allocating objects and spreading them can have a significant performance impact. (This is coming from someone who maintains a competing virtual DOM framework that's structurally very similar.) If you really want a perf comparison, [here's my above polyfill benchmarked with my example vs yours](https://flems.io/#0=N4IghgrgLg9gSgUwDYzAExALgGZiQZwQBoRsBLJBfLAbVADswBbBLEAOgAsomkQSAxjHpQEItiAC+RBs1aYQLJjDIAvBAFoYAByj4N+FAHd2AK2qDho8QoD0tgAQBlY0Qf5tCAQ7JNtlFhEwKDJhAB16IzJ6NBgTJRV1AHldfBc4hwBeBwAKHIBKLIA+B2AIhwqHMJAIQncoACcyAShqiPLKoXp8KAcACSSAGQBRLNLJDorsCHoW0PoHAQaEYIQc7TBlkTcAawQAT0KyhcrT5agIBoXgDa2oXYO3JjBtTAd6BCMHAFkXgrcAG54CAIN4DEYTE4VSGTBzTWYhYQOBJqBApPTpIw5eECNw6PRHWGnMjYXJQfaeGCknEOACEmWy1RxiPo1UJUNOnKgnAaGQ+XwAKhSEMMGryGjkAOTcsj4ZG1XoAIwQizwSDAisokvyRMqMI5p0ovUIlBabgQAOaUAAgthRA1dRUSbl8XL6e8IEgkOzOb73MgvL1sq72CbA47Oc6cmGWnTsvRPUgHAAyZMOcmU0kx3r07KS5nzbWlCN+9M8vmfBxCzyi8VSkPZ+U9BzK1VejWUHyk7TLQgibUlvXtA1+i1W232sYhsctCcIB0j31Rmc2u3zuMer0ptMZhBUhwrucNON5gvCIvHUul7m8r78qvC2swCWS6eW2dr49MBUtlUCNUdiqzo9lQYhQAOi6cpCpb6qWXTNryMBQAAwmAAicCq2RLCsog5DMaAIOQHxoG4+GEdECBoDqrKQeclwLGeCwFMWkGnPBvT-uhCAAArBJwYyNu6CZeoOvoAPwODQ7DSdm7AvP4+w5DK+BuJsADmECBASAC6omcm8NDKW40lyQ0Glafguk0VelRGqqXFjIhKFoRhenYM+uTsQ4ez7A4+6cRhvHcj6NmcnZ6EUGgYwBQg7DPNo7BqQgUA5D51GhZGpI5BFSBRQym7eixGVwZwkXRcsqzZS5xDeQc6XFb6MVxS8obJaljyLKVuX1Q10ENfZGHRV1aB6bBoVRk1QJICCJ79EMwwhQ1dFXANsVTSCo3Dhl9gOMMAAeXjQJRv7ucsZYqmhFx4HCMxzEi+AwGW0RqXKSBkHs6a+Hu0BypsCB6Tt+BcWgnrHWhAiXKsSD7Hpy7vquk5CYmi3FTkYD4Pssy5IUmQlJe-WnGARhgGQvSHp+cnaApSmlSpDjqZpYH4D1BMOOFlx3NF1V6XBHNgew62YXNIw836O3ITAXqBg4ambIqYBJaLvpGF1KrZXzIizU5qEOamdIQ2K-PxaGqIo6znQa1A7C3EbLUEUaawG3c7BpUrV5O2B0WW9bf0iG7px9b1+QFJt1mhctCyTcCQs4pT1NGfTZmMyIzMRoHDhjRUEfIggyiouiaTGBEkjB-k-AgNm8zUAoABsAAcmAACxSDIICMCwbAouoWipBouA9GYFggF01hQGw4soIQx6wA4KvBAe9BqRRcpGDAnpRZdEBqr5vj+LnYERFEMRxHFueJGiqQAGLo0GuTMbjRWVNUtQqj0TQtG0YcVF54KjNkwDp0YosCquEbbbFqocR+vps43F9vcCBTwXhvHvL8bQ-wHCCzBPNdOmcboInmOmBo+xhjwyqlxNwPYYBMFlAgM2lRKHUMIOwIi288g4zxnpdmhtNZYWqhQuBXMuI+zuHpD2IgBbRzGL-AGjgJZS1jLLBo8tFasUqCrCgaswE5njImbc+tvbG3wKbKBxUtHNQSvbZKjtvau1UY1S2YwtFuy0Y4uBwiD52IzmnFmuCgFdwvnoa+PRsS3TxKkOhTosq7n3DSXMVRSC3RZGyEx15yx3krNWEUYpnxSmUk2JUf4AKagQBBGCW0-R2WzOaeGR4IxRldBuYShV8Y2UbMGVIrVTStE8VGQSOitx62iVmAMsY4n5kSYWCJV4bwVkFI+bJL4GwjN6N+ZsrZ-ztmKV2BwIE+zgRZmUr+V5yaTnaXodgJz5ywyypc48SMBk7mFPuW5s1xn4PPFM1Jt53gZPmXWV8HSXmrIKW2dUWzgK9jAqU0K6cA7lMasIBCMAkI60GlhEBawyJEUoqRGI5FiLUQjNnPpBU9ISRefcpAbsJJAOYi01m4VqqOWRc5Li-tKinU8oi3oPk-LDK6XHaGNNZSqSThZfInz+rhWGoIjC5jEptVsebJcWUcp5X6c09lNk1XlRwo7PhECDnKrYtVeVhAUo+TcGqo15tYXKpikNSKWqvFHIJhNU1gtZq-0lUtZK9FVoSOmv9TxhznU7X2odUQUVlSnRVNyC6LQt5JiAQ9J6i9XrvWDcanaIQWBrz0InV+wNQYbwEAbKGMMQ1+kaMQ0hzqTXkPrZUNGGMsb3xKLcwVikE4M3FaXZ1hKq3QL9StKOQaxix3kkKntYqmY2qvHamybxaU+qvIyhy2RtbcyHZyTl2VuUQL5f6AVU7u201FeZOdq6MrSrKrwoRxskoWrqk2yJnkZX5Sade-qOr0V6rIRhB4hxX2ciaoYxVHVrUgcXazB1WFhrOtwW61VHrJFxO9Sk5V2cx0bR3XC11-VsOofHdkSdVNp3nsTpelO87DkZTeBSjV1K8F3SYt+30660UOC3WyvDtlkqdTvQGx9bVlK0fGqqj9Gr2Puw-cA-9MU3BiZA2Blq5rhV0yg3x6Ezq4OCdys6vdXleX7l7XOzD5tb25VlbFETz7gPacyu+u90mLPGv03leTlVFOGpAw2uV4H7NWuGuJ3qKmmXwadY5pD-V3VCM9eh+aMnSxEfi9HRD8LWbhoOhDKNJ1nxxsGpva6KbHoynTWzTNYbHC5u+gWv67hi2UFLeW0Q0NnU1pIVaHI4XG2OdOC2zG3h20HhqRTU9GmL3JwJP2xzg7jWpblZ60jt0u2Tao9N1OO6YOlmXRMpEdLnWcaFjx1yjmrOedUwlJ9GnQtXgmlJgqyW-S-q87hHzyn+udFNYF9bWnjU7dCnptVhmPL7u6Dyg4R6zM0bcwTC7Nn5U3aVe5t92VHtfrh-auT2FvMGpR6j-ztm1MQf2MFyKd2MqA4asDhD0XMvIc8sRmaiWRjPeHRcUdzOs3Kpi76znkducTtWxNmd1GCRuxwZl7O-iC5BO6SXAo5dK6IrYAARkbgAJibi3WQHcFD+IMLvSgg9y4jzAuPRwABNNex4ATziMeESI0RYjxDPqiJwxuhZsOKCY5+dQ35Wk-rCH+80xgANhEA3HoC4FAamTArRQHEGvB+V8VB6DMHC2GFLo5QCbuJ8NW5hH2QzF2fag50sD2XNPax9-HHGL1ix98540vJOgsefnTB7DdP8OR-2wsfxnu-CUBCbMKZUYhksdmkyfvyT6VfNmQ+GsCzcm03yb+UFgFoW+j54sA9p2hbR8xXi7FJEHBYoolRBnlRs4rtr2zATemD+cMfzjn7LUbufYylX6zn7kb37YnrwUwNS-x-XfwSnUwTn+xhVEQiw8zcjB2Myh1M1nVh3n1CmL2Ew-1J0p0qB-3VRr3QJpyALx3IWb2NSu1anb2gLCx3Vpyiyp2vxVSZzS3HVZwWgAJvxHUF1YNw0YIIw539RwxjhF3IzPRFQ23FTTlhFwRl3d3UCHz3mLlLmV2WSrjYEbgAE5MAAAGXXNuOQNgU3SwEQC3OwRwAUIrRNbeBwUwCAZoXyHoCAbAbAdgQ+F3E+YQFAdAMYEbelf3V+RoIPEATLIBUQHoAAaQOEvlujRigEaGZjc2zmCASOYWRQcAAB4fg+J2AGgwBj4mBmI9YUiGh8B2B5ZjwsjfhuRcj8jYhCifFMsdoAADdyGAZo+mGIBwZoiojooQdeSre0Gw1sfI-YFWecWKfoOIC0ecNwAASVVAWGWGhj8gWGfAIgaDcI5DCKoCgCcBAnQBiNmEO0gi8koAWGyBh3OXOLUm5AcEcE1wjDsh8jmLxT2jGB0MBGjleIIneOyHOIjHUU7HLx+IQHeKyPONXXwCiCgHQlyCuPwBoBeLeO0hk3-DqCZGRWqEwDdnRJVGqAqOxLdijARJoEFlBL2m0kyOyJqLyIKIKEKGzkaD4JgM8WRN+IAGoOS9JyS3iuTpDaJuC4Q8BCAZDMsvJOB0YkgjALiHAkhFRTBAxrZeRYBdwuApSZTuJeRPAGhyQQ8D1wi9jjAoj9g5jTkc485khUhMQlJdiTSjjcQ3Nsw9t3kFgAAqOIhI1dIzA9XlaIemeI0omTKMSU-AaU+gdgDZJAT00ouPdnBwfYMgZAKKHyZ1RM5MgMhIpEg4KyAmanF1OjPUebToA03Y+XE0s09cbIWXK+G+W0yI6I0JJ05ZF01jBwD0koxIog3dRA30qHf0zs4MrKUM8MyMtUGMumNKeM9M6zVMxzGcjeQMxEnyXM-qfMmDHBYs7+UsnoTEE0-YiqNAV4j4Y8as+QgJQuOIesvYg4tAB0lmHY3c40g4A8lYNACc+PIUw0vcl82848+cHIEyeUxUloF2A4fAD84yaSYCpUwWCCwcxoo5LyQ08s38w8-808i08+OXOs7828+8vvV09MMsm+fc28j8pIr8kihs-YV89ADCwC6ChUpUnyeCpc-IKC9gGC0CuCj8xC-UiHYi3cr3Oio8+gE8sYQfL3a80Sgi7Y-vISvYkS8iwcyigXRSxQygUShioC5i0C1iyChwXSkCq2XihCsUpCg9fAew0QMYe8AAITEHQmeAaB2HYCcBsrWC3L30EsNO4oLWyBoAjGADaLeDVzcAqLeE12kGCtCocHCpbDAFUDeAAGYIrNgoqYrIIygQA55eh41sT4k3SAABWkAAYmqDcDiuqDV0qsSoaDeGqEeP4ESuSviRSuqCytLByryvOkKuqBKvKrqvljauqA6pauqpAFqpasiviWaq6r9GABGtSvSoaocE1yquRTCoWt9BCq2vitWqivSrapSp2s5CWoyvWs2pgDCuOtSrOtOAurWo2rhH2rVwesqD2puoOvqsyqIGCuWocDSteu+oStmuiv+uysBuBrirBsuohuCuqF6oKqwCKtKoqpmsuqarqsmumruvas6shu6qRslPytclRoGvRuGqxpAGavxrGpxv2pqsJsRtytJr6oppAEGoxvppAHGsOrmsZu+uZpbgBvhuututavuqJsWpJvnhRsaq5qpsxtGr5qFsVrxt+sFtFuyrlrJv+k5u5vVviU1tmuxp1uJrZvlvJsVqNsxrWvNolpNpZuythoFpeuho+oqC+sluhvdq9uLCtv1v6qVqGomqZqmupodtpuptVvGoDp6vZoVrRrDoFsdqloJvDuFsjottlqDo5ttuVt5v5q1vTtxpdu6r9q1pethoTr1oLpTp5pBo1tjsVpLrNpjtzt2vBvxphrerrvzuTsptTubudvtsVvmplt2vrqHtDqbvLpVrbqjonorrzuRptsbuXu1qdpFoTp7ozr7tBr3vFtHveqnvOpno3uHqbo7rptHpFuLtXoqCslhDskNIpI+MIrbL8r0pSimS8hdzBLGHfreIjBAd+N8PAaAY5PisKDTBStqsFPUp-pMsRMAcpIsthGstJlinQHfOqEMAyG0Eln2HIC3BgezDqr8MBI8JMENOAd2J-NNPtGvP8oZJkO8uwdEDkjQHwZAHxF8FRCimIehjIaTAoeWSofYTcyPld3YHoeyBQtIoOErIlBQcDHYY5BLkyy4dwd4Z6wrmMB2RIbEYcAodvKkd927NkZPgUY0ufNovIvUZaE0YDk4c8p4b4YEeoXUGEZMYoHEfcAsZauocghsbod2IYZ6FQsccPNYd-tcaLJ0Y8bwYMaMWHxVCBCaHyN6HMcPMsYfmsdofkcicUcYeUriecb-p8XcZwfYGEAMYEH2AEEoDqpmJ4Q4VOMRUllihQDUhyHaatigHUmSnkZgCcCCMXkSehFqe4YaeqCEAydEAKc6bgm6ZNz6YMfl0idlEVrMfcA8bIZYaZBvl2LZHMQMfbgNolQ4fFPWd6ZgH6eqDgBmHoGehMjZCwY8YaBmByGAHRiGzeGZIQG0foEkFUK6XUIUDV01wAHYm5m5pA9d5AOABB8Ah5zcbAQApBtISA3p6Adhq46ADD9cQAUA0B0ZOBy5Lg+AFBuAoBtB8BMB7AZhtAdg1JIyqFbByXKXirG52A1dYX2BNcdDuWYAKX8BOBjCQBdw2AgYmhdB9Crm2BlRZhOAXKdhqWGhaWQB6XGXmXbBWX2XOWmBbBVXnLNgdhirNcBX2BG4zWnL1XLXpXZWFB5WyBFWSAVdug2BNc64dD4WUqABWHFyQIAA). And here's the ranges I got from that, in order of performance:

1. optimized polyfill + select: 2,520,046 to 2,686,660 ops/sec
2. slow polyfill + select: 1,281,794 to 1,343,756 ops/sec
3. optimized polyfill + spread: 1,042,992 to 1,111,727 ops/sec
4. \*simple variant + spread: 990,946 to 1,059,852 ops/sec
5. slow polyfill + spread: 916,751 to 980,107 ops/sec

\* = yours (rest are all mine for show)

This kind of perf boost (native implementations would exaggerate this cliff, BTW) makes the spread variant a _lot_ less attractive.

> [@NickGard](#):
>
> I like the idea of cache-busting, but it should also be a **hint** to the JS engine that the cache is no longer needed.

I intentionally avoided that as it's an area of non-determinacy, something I'd like to avoid and something I know TC39 members aren't fond of, either.

Also, things like network cache expiry is something you _don't_ want to just leave it at the whim of the engine to decide.

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [October 3, 2020, 12:36am UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/11 "2020-10-03T00:36:35Z")

</div>

> [@NickGard](#):
>
> This has the unfortunate knock-on effect of checking if `clearCache` exists on any given function (and is callable) before invoking. I think an API similar to `setTimeout` and `clearTimeout` would be more robust:

FYI, the chances of that working are slim for the same reason `weakMap.clear()` was left out as being redundant with `weakMap = new WeakMap()`.

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [October 5, 2020, 8:01pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/12 "2020-10-05T20:01:57Z")

</div>

We may be overthinking this on the cache-busting. Part of the purpose of this proposal/idea was to let the JS engine determine if trading time for space was worth it. The author shouldn't have to worry about cache management whatsoever. I say we get rid of any references to `evictCache`, `clearCache`, and `dememoize`.

I'm coming around to the idea of a selector function, in place of the hashing function I listed in an earlier post. I like the idea of the author picking the key(s) to cache by because they'll know if two different (meaning referentially unequal) sets of arguments will produce the same result. I'm not sure if returning an iterator is a great idea. Here's an example of how this could go bad:

```javascript
const arr1 = [0, 1, 2];
arr1.meta = 'created on 01-01-21';

const arr2 = [0, 1, 2];
arr2.meta = 'created on 02-02-21';

const getMetaData = Function.memoize(
  (array) => array.meta, // the function to memoize
  (first) => first, // only key off of the first argument, ignore any extras
);

// first run, unmemoized 👍
getMetaData(arr1); // -> 'created on 01-01-21'

// second run, memoized 👍
getMetaData(arr1); // -> 'created on 01-01-21'

// new argument, but shallowly equal 👎
getMetaData(arr2); // -> 'created on 01-01-21'

```

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [October 5, 2020, 8:13pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/13 "2020-10-05T20:13:18Z")

</div>

I ran the perf comparison in Firefox v81 and the spread version was faster there. The select version was faster in Chrome v85.

## Firefox v81

![image](https://global.discourse-cdn.com/free1/uploads/es/original/1X/0a8739f06c0851bb6d174ef46c3f969ab482ac36.png)

## Chrome v85

![image](https://global.discourse-cdn.com/free1/uploads/es/original/1X/b762a4898bfd2712b9e16f6d2ba4f6e51ca0d46b.png)

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [October 26, 2020, 12:48pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/14 "2020-10-26T12:48:41Z")

</div>

I wrote up a polyfill of Function.memoize using WeakRefs ([https://caniuse.com/?search=weakref](https://caniuse.com/?search=weakref)) and a cache trie. The main point of this implementation is not the speed of memoization or retrieval of memoized values, but eliminating memory leaks where memoization caches hold on to otherwise unreachable objects, preventing them from being garbage collected.

I didn't do anything special with the hash function (like interpreting iterable return values as separate keys as @claudiameadows discussed) but that is something that could be further explored.

After implementing this, I realized that the consumer could also pass in a comparison function (currently this is done with `===` in the `get()` function) but that may be a footgun since doing deep or complex comparisons on so many values may be slower than the computation that was memoized away.

I also had difficulty in writing tests to determine if the cache had been modified by `pruneTree()` after a value had been deleted, so I relied on breakpoints in the browser DevTools to confirm that it worked as expected.

```javascript
// Polyfill
Function.memoize = (function() {
  const NO_VALUE = Symbol("NO_VALUE");
  const VALUE_AT = Symbol("VALUE_AT");
  const registry = new FinalizationRegistry(function schedulePruning(
    cacheInstance
  ) {
    // An object key in the cacheInstance has been Garbage Collected.
    // Schedule a cleanup of the cache at the next idle period.
    window.requestIdleCallback(() => pruneTree(cacheInstance));
  });

  function isWeakRef(wr) {
    return Object.prototype.toString.call(wr).slice(8, -1) === "WeakRef";
  }

  function createCacheNode() {
    const cache = new Map();
    cache.set(VALUE_AT, NO_VALUE);
    return cache;
  }

  // remove all branches that are keyed off of empty (GC'd) WeakRefs
  function pruneTree(cache) {
    for (const [k, v] of cache.entries()) {
      if (isWeakRef(v) && !v.deref()) {
        cache.delete(k);
      } else if (k !== VALUE_AT) {
        pruneTree(v);
      }
    }
  }

  // essentially Map.get where cache is a map with some WeakRef keys that
  // need to be dereferenced to be checked, and the value is a cache node
  function get(cache, key) {
    for (const [k, v] of cache.entries()) {
      const _key = isWeakRef(k) ? k.deref() : k;
      if (_key === key) {
        return v === NO_VALUE ? null : v;
      }
    }
    return null;
  }

  function findValueByKeys(cache, ...keys) {
    let currentCache = cache;
    while (keys.length) {
      currentCache = get(currentCache, keys.shift());
      if (currentCache === null) {
        return NO_VALUE;
      }
    }
    return currentCache.get(VALUE_AT);
  }

  function setValueAt(cache, value, ...keys) {
    let currentCache = cache;

    // traverse to the leaf node
    while (keys.length) {
      let key = keys.shift();
      let nextCache = get(currentCache, key);

      if (nextCache === null) {
        nextCache = createCacheNode();
        if (key !== null && typeof key === "object") {
          registry.register(key, currentCache);
          key = new WeakRef(key);
        }
        currentCache.set(key, nextCache);
      }

      currentCache = nextCache;
    }

    currentCache.set(VALUE_AT, value);
  }

  return function(fn, hashFn) {
    const rootCacheNode = createCacheNode();
    const shouldHash = typeof hashFn === "function";

    return function() {
      let args = shouldHash ? hashFn(...arguments) : arguments;
      let val = findValueByKeys(rootCacheNode, ...args);
      if (val === NO_VALUE) {
        val = fn.apply(null, arguments);
        setValueAt(rootCacheNode, val, ...args);
      }
      return val;
    };
  };
})();

/ ********************************** /
/* Tests */
/ ********************************** /

const ctx = { maybeExists: { val: "foo" }, alwaysExists: { val: "bar" } };
let unmemoizedRunCount = 0;
function combineValues(a, b) {
  unmemoizedRunCount++;
  return (a && a.val) + (b && b.val);
}
const memoCombo = Function.memoize((a, b) => combineValues(a, b));

memoCombo(ctx.maybeExists, ctx.alwaysExists);
console.assert(
  unmemoizedRunCount === 1,
  "combineValues should have run unmemoized"
);

memoCombo(ctx.maybeExists, ctx.alwaysExists);
console.assert(
  unmemoizedRunCount === 1,
  "combineValues should have not have run"
);

memoCombo(ctx.maybeExists, ctx.alwaysExists);
console.assert(
  unmemoizedRunCount === 1,
  "combineValues should have not have run"
);

delete ctx.maybeExists;

memoCombo(ctx.maybeExists, ctx.alwaysExists);
console.assert(
  unmemoizedRunCount === 2,
  "combineValues should have run unmemoized"
);

```

### EDIT: Better polyfill

```javascript
Function.memoize = (function() {
  const NO_VALUE = Symbol("NO_VALUE");
  const registry = new FinalizationRegistry(function schedulePruning(
    cacheInstance
  ) {
    // An object key in the cacheInstance has been Garbage Collected.
    // Schedule a cleanup of the cache at the next idle period.
    window.requestIdleCallback(() => pruneTree(cacheInstance));
  });

  function isWeakRef(wr) {
    return Object.prototype.toString.call(wr).slice(8, -1) === "WeakRef";
  }

  function createNode() {
    const node = {
      cache: new Map(),
      value: NO_VALUE,
    }
    return node;
  }

  // remove all branches that are keyed off of empty (GC'd) WeakRefs
  function pruneTree(cache) {
    for (const [k, v] of cache.entries()) {
      if (isWeakRef(k) && !k.deref()) {
        cache.delete(k);
      } else {
        pruneTree(v.cache);
      }
    }
  }

  // essentially Map.get where cache is a map with some WeakRef keys that
  // need to be dereferenced to be checked, and the value is a cache node
  function getNode(node, key, comparisonFn) {
    for (const [k, nextNode] of node.cache.entries()) {
      const _key = isWeakRef(k) ? k.deref() : k;
      if (comparisonFn(_key, key)) {
        return nextNode;
      }
    }
    return null;
  }

  function findValueByKeys(node, comparisonFn, ...keys) {
    let currentNode = node;
    while (keys.length) {
      currentNode = getNode(currentNode, keys.shift(), comparisonFn);
      if (currentNode === null) {
        return NO_VALUE;
      }
    }
    return currentNode.value;
  }

  function setValueAt(node, comparisonFn, value, ...keys) {
    let currentNode = node;

    // traverse to the leaf node
    while (keys.length) {
      let key = keys.shift();
      let nextNode = getNode(currentNode, key, comparisonFn);

      if (nextNode === null) {
        nextNode = createNode();
        if (key !== null && typeof key === "object") {
          registry.register(key, currentNode.cache);
          key = new WeakRef(key);
        }
        currentNode.cache.set(key, nextNode);
      }

      currentNode = nextNode;
    }

    currentNode.value = value;
  }

  function defaultCompare(a, b) {
    return a === b;
  }

  return function(fn, hashFn, comparisonFn) {
    const rootCacheNode = createNode();
    hashFn = typeof hashFn === "function" ? hashFn : null;
    comparisonFn = typeof comparisonFn === 'function' ? comparisonFn : defaultCompare;

    return function() {
      let args = hashFn ? hashFn(...arguments) : arguments;
      let val = findValueByKeys(rootCacheNode, comparisonFn, ...args);
      if (val === NO_VALUE) {
        val = fn.apply(null, arguments);
        setValueAt(rootCacheNode, comparisonFn, val, ...args);
      }
      return val;
    };
  };
})();

/ ********************************** /
/* Tests */
/ ********************************** /

const ctx = { maybeExists: { val: "foo" }, alwaysExists: { val: "bar" } };
let unmemoizedRunCount = 0;
function combineValues(a, b) {
  unmemoizedRunCount++;
  return (a && a.val) + (b && b.val);
}
const memoCombo = Function.memoize((a, b) => combineValues(a, b));

memoCombo(ctx.maybeExists, ctx.alwaysExists);
console.assert(
  unmemoizedRunCount === 1,
  "combineValues should have run unmemoized"
);

memoCombo(ctx.maybeExists, ctx.alwaysExists);
console.assert(
  unmemoizedRunCount === 1,
  "combineValues should have not have run"
);

memoCombo(ctx.maybeExists, ctx.alwaysExists);
console.assert(
  unmemoizedRunCount === 1,
  "combineValues should have not have run"
);

delete ctx.maybeExists;

memoCombo(ctx.maybeExists, ctx.alwaysExists);
console.assert(
  unmemoizedRunCount === 2,
  "combineValues should have run unmemoized"
);

```

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [October 31, 2020, 8:14pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/15 "2020-10-31T20:14:41Z")

</div>

I made an updated polyfill without memory leaks. No objects were referenced in the making of this polyfill. It also has options for a `select` function, a `hash` function, and a `compare` function

```javascript
function MemoryObserver(callback) {
  if (!new.target) {
    throw new TypeError("calling MemoryObserver without new is forbidden");
  }

  let cancelToken;

  function notify() {
    // jsHeapSizeLimit
    // totalJSHeapSize
    // usedJSHeapSize
    callback(performance.memory);

    // check again later
    cancelToken = window.requestIdleCallback(notify, {
      timeout: 500
    });
  }

  this.disconnect = () => window.cancelIdleCallback(cancelToken);
  this.observe = notify;
  this.takeRecords = () => [performance.memory];
}

Function.memoize = (function() {
  const NO_VALUE = Symbol("NO_VALUE");
  const cacheInstances = new Set();
  let isSystemMemoryConstrained = false;

  const memoryObserver = new MemoryObserver(
    ({ jsHeapSizeLimit, usedJSHeapSize }) => {
      const shouldBeConstrained = usedJSHeapSize / jsHeapSizeLimit >= 0.8;
      if (shouldBeConstrained && !isSystemMemoryConstrained) {
        flushCaches();
      }
      isSystemMemoryConstrained = shouldBeConstrained;
    }
  );

  memoryObserver.observe();

  const registry = new FinalizationRegistry(function schedulePruning(
    cacheInstance
  ) {
    // An object key in the cacheInstance has been Garbage Collected.
    // Schedule a cleanup of the cache at the next idle period.
    window.requestIdleCallback(() => pruneNodeCache(cacheInstance));
  });

  function isWeakRef(wr) {
    return Object.prototype.toString.call(wr).slice(8, -1) === "WeakRef";
  }
  function isNonNullObject(o) {
    return o !== null && typeof o === "object";
  }

  function flushCaches() {
    cacheInstances.forEach(wrappedCacheInstance => {
      const cacheInstance = wrappedCacheInstance.deref();
      if (!cacheInstance) {
        cacheInstances.delete(cacheInstance); // remove empty wrappers
      } else {
        cacheInstance = createNode(); // effectively delete the entire cache
      }
    });
  }

  function createNode() {
    const node = {
      cache: new Map(),
      value: NO_VALUE
    };
    return node;
  }

  // remove all branches that are keyed off of empty (GC'd) WeakRefs
  function pruneNodeCache(nodeCache) {
    const cache = nodeCache.deref();
    if (!cache) return; // the node no longer exists
    for (const [k, v] of cache.entries()) {
      if (isWeakRef(k) && !k.deref()) {
        cache.delete(k);
      }
    }
  }

  // essentially Map.get where cache is a map with some WeakRef keys that
  // need to be dereferenced to be checked, and the value is a cache node
  function getNode(node, key, comparisonFn) {
    for (const [k, nextNode] of node.cache.entries()) {
      const _key = isWeakRef(k) ? k.deref() : k;
      if (comparisonFn(_key, key)) {
        return nextNode;
      }
    }
    return null;
  }

  function findValueByKeys(node, comparisonFn, ...keys) {
    let currentNode = node;
    while (keys.length) {
      currentNode = getNode(currentNode, keys.shift(), comparisonFn);
      if (currentNode === null) {
        return NO_VALUE;
      }
    }

    // unbox value
    return isWeakRef(currentNode.value)
      ? currentNode.value.deref()
      : currentNode.value;
  }

  function setValueAt(node, comparisonFn, value, ...keys) {
    let currentNode = node;

    // traverse to the leaf node
    while (keys.length) {
      let key = keys.shift();
      let nextNode = getNode(currentNode, key, comparisonFn);

      if (nextNode === null) {
        nextNode = createNode();
        if (isNonNullObject(key)) {
          registry.register(key, new WeakRef(currentNode.cache));
          key = new WeakRef(key);
        }
        currentNode.cache.set(key, nextNode);
      }

      currentNode = nextNode;
    }

    currentNode.value = isNonNullObject(value) ? new WeakRef(value) : value;
  }

  function referenitalEquality(a, b) {
    return a === b;
  }

  return function(fn, opts) {
    const rootNode = createNode();
    const hash = opts && typeof opts.hash === "function" ? opts.hash : null;
    const select =
      opts && typeof opts.select === "function" ? opts.select : null;
    const compare =
      opts && typeof opts.compare === "function"
        ? opts.compare
        : referenitalEquality;

    return function() {
      const keys =
        hash && select
          ? select(hash(...arguments))
          : hash
          ? [hash(...arguments)]
          : select
          ? select(...arguments)
          : arguments;
      let value = findValueByKeys(rootNode, compare, ...keys);
      if (value === NO_VALUE) {
        value = fn.apply(null, arguments);
        if (isSystemMemoryConstrained) {
          // in a constrained environment, only store the last set of arguments
          rootNode = createNode();
        }
        setValueAt(rootNode, compare, value, ...keys);
      }
      return value;
    };
  };
})();

```

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [November 8, 2020, 12:18am UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/16 "2020-11-08T00:18:30Z")

</div>

I put together a readme and polyfill at [https://github.com/NickGard/proposal-function-memoize](https://github.com/NickGard/proposal-function-memoize).

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 28, 2022, 3:34pm UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/17 "2022-09-28T15:34:31Z")

</div>

TC39 has a stage 1 proposal for function memoization now: [GitHub - tc39/proposal-function-memo: A TC39 proposal for function memoization in the JavaScript language.](https://github.com/tc39/proposal-function-memo)

---

<div class="post-metadata">

**Author:** ![bergus](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/bergus/32/152_2.png) [@bergus](https://es.discourse.group/u/bergus)\
**Post date:** [September 29, 2022, 12:41am UTC](https://es.discourse.group/t/built-in-function-memoization-with-weak-references/446/18 "2022-09-29T00:41:39Z")

</div>

I don't see much use in that proposal. Function memoisation is a niche. It can be trivially implemented on top of a `Map`.

What would be real benefit to all ECMAScript developers would be advancing on the `Map`/`Set` features, like [GitHub - tc39/proposal-policy-map-set: A TC39 proposal for Maps and Sets with cache replacement policies like LRU and LFU.](https://github.com/tc39/proposal-policy-map-set) or [Custom hash and equal functions for Map and Set collections](https://es.discourse.group/t/custom-hash-and-equal-functions-for-map-and-set-collections/673) e.g. [in the collection normalisation proposal](https://github.com/tc39/proposal-collection-normalization/issues/18) or a simple `WeakRefMap`.
