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:
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:
You could similarly have a WeakMap that stored references the other way:
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:
Then I defined one WeakMap, and in that map, I required both
the "wet" and the "dry" values point to the submap
object:
There are a number of advantages to this approach, in my view:
- I can easily define a "damp" object graph value on
submap
, by simply adding a new property tosubmap.proxiedFields
. Similarly, there is no limit to the number of object graphs I can define. - 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. - 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.
- I can generalize
submap
into aProxyMapping
constructor, and define methods on theProxyMapping.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
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:
- Different levels of privileges or security clearance, with different API's exposed to the proxies
- A common base set of proxies, and then special extended sets of proxies for specific sections of the object graph (example: shadow content of Web Components should only be available to the code owning the component)
- A function broken up into different phases: precondition,
postcondition, argument validation, main body
- Special case: logging entry and exit
- Special case: class invariants when body doesn't throw
- Think aspect-oriented programming, in a possibly ugly mess
- This doesn't allow code running between lines of a function - at least, I don't see how that would be possible without an interpreter like ye olde Narcissus.
- A "read-only" view of an object graph, where .defineProperty() returns false every time. (As opposed to a hidden or "whitelisted" object graph.)