javascriptmetaprogramminges6-proxy

What would be a use case for identity-preserving membrane proxies?


When I was reading about ES6 Proxies, it seemed simple enough to understand until I had taken a look at this example.

I'm stumped. I don't understand the "wet/dry" terminology that they use, and I have no idea when I'd end up in a case where this would be the ideal choice, especially since I can't seem to find one.

Could someone provide a short explanation on what kind of scenario where this would occur?


Solution

  • First off, a little bit of basics: an object is a collection of properties (some of which are functions, and officially named "methods"). This may sound obvious, but it's important: you refer to other values by the object and a property name.

    Proxies allow you to rewrite the rules for referring to other values by that tuple of the (containing) object and a property name. For instance, you can hide "private" members behind a proxy.

    But suppose you have a cyclic object reference. For instance,

    var x = { z: function() { throw new Error("This shouldn't be callable"); };
    var X = /* proxy representing x, where X.z is hidden and cannot be called */;
    
    var y = { x: x };
    x.y = y;
    

    Or in Document Object Model terms, document.documentElement.ownerDocument == document.

    In a normal proxy, referring to X.y would return y. Nothing really wrong there... except that X.y.x === x and X.y.x !== X. So I could still call:

    X.y.x.z(); // throws new Error("This shouldn't be callable")
    

    Membranes are all about making sure that non-primitive properties (objects and functions) preserve this identity relationship, and maintain whatever proxy rules you had in place through any combination of property lookups. Membranes keep you from accidentally crossing into direct access to the underlying (possibly native) implementations of various objects.

    If X were a membrane-based proxy of x, X.y would not return y. Instead, it would return a proxy of y, which I will call Y. Y exposes properties of y, much like how X exposes properties of x.

    More importantly, suppose I refer to X.y.x:

    X.y.x === X; // true
    X.y.x !== x; // also true
    typeof X.y.x.z // returns "undefined", not "function"
    X.y.x.z(); // throws TypeError("X.y.x.z is not a function")
    

    The reference to X (the proxy for x) is returned through the membrane. Thus, the identity property is preserved. (x.y.x === x, so X.y.x === X.)

    Here's the most important concept: A membrane means you may never see the original objects, only the Proxy objects that represent them.

    var X = (function() {
        var x = { z: function() { throw new Error("This shouldn't be callable"); };
        var y = { x: x };
        x.y = y;
    
        var X = /* membrane proxy representing x, where X.z is hidden and cannot be called */;
        return X;
    })();
    
    X.y.x === X; // still true
    

    In Tom van Cutsem's articles on JavaScript Membranes, one of which you cited above, the values x, y, and x.z could all be considered part of the "wet" object graph, while anything referred to via X or Y would be part of the "dry" object graph. (The term graph here is from graph theory, a part of the study of discrete mathematics. Object graph means a set of related objects, and the membrane is what separates the set of "native" objects from the set of proxies to those objects.)

    The x and y values are inaccessible directly from outside the function. (In JavaScript parlance, they are local variables in this example, but that is partly misleading from the standpoint of a membrane: if we were talking about DOM documents, what you are really getting in a web browser like Mozilla Firefox is a proxy to the DOM document, not the actual document object from native memory. Whether it's local variables or values inserted into the scope of the JavaScript you're running, a membrane and its proxies won't care.)

    Instead, the only access you have to x, y, and their properties is through the X membrane proxy, and any properties you get from X. Because it's a membrane, that access is always indirect.

    As for a scenario where this would happen: suppose you have trusted code that can do all sorts of things, like access the computer's file system. You do not want webpages to be able to directly read from the file system, or worse, write to it. A membrane of proxies, by only exposing the properties and methods you intend to expose, reduces the API that a webpage could use, so that accidental access to that trusted code is much less likely. This makes security exploits much rarer, then.

    Best of all, if the membrane implementation is correct (which is much harder than it sounds), the webpage JavaScript doesn't know or care that it's dealing with proxies. The webpage script thinks it's dealing with an ordinary DOM. That's what we want.