Objects, WeakMaps, and Proxies into a Membrane: The es7-membrane design

One-to-one relationships in JavaScript

In the early days of JavaScript, when you wanted a hashtable-like relationship between a key and a value, you were very limited: you could define a JavaScript object and store (key => value) relationships on that object, but the keys were transformed into strings by necessity. It worked, in other words, if all your keys were strings that weren't reserved property names of an object:

"wet1" ⇒ dry1 "wet2" ⇒ dry2

This wasn't terribly useful, but it was the only option available for many years. Later came WeakMap objects, which added objects as valid keys, provided you used the .get() and .set() methods to pass in both key and value:

wet1 dry1 wet2 dry2

You could similarly have a WeakMap that stored references the other way:

dry1 wet1 dry2 wet2

This requires, then, two WeakMaps, and defines a proper one-to-one relationship between objects in the "wet" object graph and objects (or proxies) in the "dry" object graph. But this is not the approach I took.

Instead, I created a hybrid of both systems. First, I defined an object (let's call it submap for now) which stores references to a "wet" object, and also to a corresponding "dry" object (or proxy). Then, I created another wrapper object around it:

"proxiedFields": "wet" ⇒ "value" ⇒ wet1 "dry" ⇒ "value" ⇒ dry1 "revoke" ⇒ () ... "originField": "wet"

Then I defined one WeakMap, and in that map, I required both the "wet" and the "dry" values point to the submap object:

wet1 dry1 "proxiedFields": "wet" ⇒ "value" ⇒ wet1 "dry" ⇒ "value" ⇒ dry1 "revoke" ⇒ () ... "originField": "wet"

There are a number of advantages to this approach, in my view:

  1. I can easily define a "damp" object graph value on submap, by simply adding a new property to submap.proxiedFields. Similarly, there is no limit to the number of object graphs I can define.
  2. I can store flags and special proxy functions on the relevant member of the submap.proxiedFields object, such as a filter function for the "own keys" of a proxy.
  3. I can store other object graph metadata on the submap which is not specific to a given object graph, such as a reference to the next object in the prototype chain.
  4. I can generalize submap into a ProxyMapping constructor, and define methods on the ProxyMapping.prototype object, for managing access to the various object graph values, fields, and special properties.

The submap and the ProxyMapping constructor, though, does not a membrane make. It only defines a wrapper for how a native value in one object graph relates to the equivalent proxies in other object graphs. For that, we have to put the submaps and ProxyMapping objects aside.

Membranes and Object Graphs

Borrowing from Tom van Cutsem's visualization of wet and dry object graphs, let's draw a similar diagram with triangles.

W1 W2 w(D3) d(W1) d(W2) D3 p(W1) p(W2) p(D3)

W1, W2 and D3 are the "original" objects (or functions). d(W1) is a "dry" proxy to the "wet" object W1, and similarly, d(W2) is a "dry" proxy to the "wet" object W2. The "wet" object graph also has a proxy to the "dry" object graph, in the form of w(D3). Finally, the "damp" object graph has proxies to the "wet" and "dry" object graphs, in p(W1), p(W2), and p(D3).

In the original membrane designs, you couldn't have a "damp" graph. Nobody really considered it. But there are a few different use-cases I can imagine: