Skip to content

Instantly share code, notes, and snippets.

@thomaswilburn
Last active May 18, 2023 20:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomaswilburn/f6008e04615061769c623caef8497f75 to your computer and use it in GitHub Desktop.
Save thomaswilburn/f6008e04615061769c623caef8497f75 to your computer and use it in GitHub Desktop.

Development By Proxy

There's an argument that if you use private class fields in JavaScript, your code is incompatible with Vue (and other frameworks that use proxies to provide reactive data binding). That's interesting! Why is that? And is it actually impossible to use private fields with @vue/reactivity? To find out, let's take a look at how both private fields and proxies work in JavaScript, and see some possible solutions.

How Proxies break Vue

A proxy in JavaScript is an object that can stand in for another and "trap" various operations, like calling methods or getting/setting properties. They're useful for mocking or logging, because you can effectively wrap an object in a proxy and see (or interfere with) all the actions that code takes on that object, without having to actually touch either side of the original code.

Here's a quick example:

var target = {
  x: 123
};

var proxy = new Proxy(target, {
  set: function(target, property, value) {
    target[property] = value;
    console.log(`Setting property ${property} to ${value}`);
    return value;
  }
});

target.x = 456; // Log: "Setting property x to 456"

Being able to intercept operations like this is a pretty slippery ability to have, especially since you're not actually obligated to pass through the intended value in either direction, but it's not without precedent. Ruby notoriously has method_missing, and Python has its "dunder" methods, like __get_attr__ or __set_attr__. Previously, there was the ability to watch an object (but not interfere) with Object.observe, but that only shipped in Chrome and was removed a few years back. Proxies are more powerful (and potentially more dangerous--I've seen comments to the effect that they're a regular source of browser security bugs, although I am not a security expert myself).

Vue uses proxies as a part of its reactivity system, so that if you have a component x, then setting x.y will automatically trigger updates in any part of the page that relies on x.y. Because it's tracking accesses, it can build a dependency tree for any change or function call, meaning that it also knows that if you call x.mutateY() and that changes x.y, it can still update accordingly.

To make this work, especially on nested objects, Vue can't just assign a proxy to the root object and call it a day. Instead, part of what the proxy does is that it also takes over nested properties, by returning a new tracking proxy whenever you "get" a property that's a reference type (meaning not a primitive string, number, or boolean value).

That's where the breakage occurs. To see why, let's write a quick class that uses public fields and put it behind a proxy that just passes through get/set operations:

class WorkingData {
  
  constructor() {
    this.value = 123;
  }

  mutateValue(x) {
    this.value = x;
    // output this so we can check its value
    return this;
  }

}

var workingData = new WorkingData();

// create a transparent proxy
var proxied = new Proxy(workingData, {});

// change the object and check what "this" was
var receiver = proxied.mutateValue(456);
console.log(workingData.value); // 456
console.log(receiver == proxied) // true

Ah! This is interesting: why is our proxy taking over as this inside our mutateValue() method? The answer may be surprising if you haven't spent a lot of time in the old-school JavaScript mines. It may help to break down exactly what is happening when we run that simple line proxied.mutateValue(456):

  1. The language performs a lookup for mutateValue in proxied to get its value (remember, methods like this might exist directly on the object, but they might also come from somewhere in the class inheritance chain).
  2. The proxy's default "get" trap fires, it looks at the object and returns the function assigned to mutateValue.
  3. Now JavaScript sets up the environment for making the function call, including setting this to whatever value comes in front of the . per usual.
  4. The function is called with that this value, which is the proxy (not the original object) and the argument 456.
  5. Inside mutateValue(), the code tries to access this.value. this is the proxy, but that's okay, because our proxy also has the default "set" trap in place, so it still passes that through to the original object.

All of this works because the proxy is allowed to get/set workingData.value, since it's a public field. It doesn't matter that the this value for mutateValue() was not the original object, as long as the proxy serves as the middleman, because it knows.

(As a reminder: while this is a fraught topic in JavaScript, a good rule of thumb is that its value is whatever object is in front of the dot or square brackets when it's called. If you don't see the parentheses that invoke the function, or there isn't anything in front of the function call, this is unpredictable.)

But what if accessing that value was gated? In other words, what if it were a private field? In that case, the proxy that sits between workingData.mutateValue() and workingData.value doesn't have permission to play middleman. And because the this value inside the method points to the proxy, not the original object, its access is revoked in turn.

Now, again, if like me you've spent a lot of time in the mines arguing with JavaScript about this values, you probably already have some ideas about how we could handle this. But before we get there, maybe it's worth figuring out a basic question: how do private fields manage their "access permissions" anyway? Why does the proxy in the middle cause this code to throw a TypeError the moment we try to access this.#value instead of this.value?

How private fields work

Some specs are pretty readable. The HTML specification, for example, is actually very articulate. It can be used as a reference by humans. The ECMAScript spec, on the other hand, is kind of a nightmare. But given enough time, and some other resources, it's possible to figure out what private fields actually do in the language itself.

Crucially, private fields (like a lot of new syntax features) are sugar over existing language constructs: we can build something that is functionally the same in older versions of JavaScript, although it won't look as nice. This is an intentional goal for features going forward, because it means that syntax can be prototyped in tools like Babel, which makes it easier to try things out and see what the caveats will be.

To make our "private" fields, we're going to need two things. First, we need a way to associate properties with an object without actually putting them on it, where they'd be accessible. We'll use a WeakMap to provide a lookup:

let privateLookup = new WeakMap();
function getPrivateField(target, property) {
  // get the field map for this object
  var fields = privateLookup.get(target);
  if (!fields) {
    return undefined;
  }
  // then get the actual value
  return fields.get(property);
}

function setPrivateField(target, property, value) {
  var fields = privateLookup.get(target);
  // if this is the first property, create its associated map
  if (!fields) {
    fields = new Map();
    privateLookup.set(target, fields);
  }
  fields.set(property, value);
  return value;
}

We also need a way to make sure that if I know the object and the property name, I still can't get access to the private field. In the spec, what it says is that each class definition must declare private fields up front, and the engine then creates a unique value for each one on each class. That means that even if two classes have private fields with the same name, behind the scenes they will still use different keys for those properties. Essentially, private keys are symbols: guaranteed unique values that won't collide with any other value in a library or other script.

Here's what our private data class might look like with those added, and our functions used for getting and setting private properties:

let _value = Symbol("#value");

class FakePrivacy {
  constructor() {
    // this.#value = 123;
    setPrivateField(this, _value, 123);
  }

  mutateValue(x) {
    // this.#value = x;
    setPrivateField(this, _value, x);
  }

  getPrivateValue() {
    // return this.#value
    return getPrivateField(this, _value);
  }
}

var privacy = new FakePrivacy();
console.log(privacy); // { }, just an empty object
console.log(privacy.getPrivateValue()); // 123 -- there it is!

So now we have an object that has private fields: they don't exist on the object at all, but inside our object (where we have the symbols and the correct this value) we can still access them. The proxy, however, can't see or access them, because it is not the correct this for the lookup, and it also doesn't have the symbol for the field (for the purposes of this discussion, assume the proxy is not in the same scope as the class definition, as in another module or an immediately invoked function expression).

The workaround (kind of)

In reality, actual JavaScript engines don't necessarily implement private fields this way. There's a very good Mozilla Hacks blog post that explains how they differ, usually because they want to take advantage of the optimizations that have already been built for making the VM fast.

But now that we know that there are two things you need to access a private field--the right key symbol and the right this value--we can start to see how it's possible to make a proxy-safe class that still uses them. After all, our proxy may not be able to access the correct symbols, but our methods and getter/setter functions do have them. We just have to make sure that the this is always correct for those functions, no matter where they're called from. We need to bind them.

class ProxySafeData {
  
  constructor() {
    this.#value = 123;
    // see the implementation below
    ProxySafeData.bindAccess(this);
  }

  get privateValue() {
    return this.#value;
  }

  set privateValue(v) {
    this.#value = v;
  }

  doublePrivateValue() {
    this.#value *= 2;
  }

  // static method for doing the binding automatically
  static bindAccess(target) {
    // get the prototype, so we know what the class defined
    var proto = Object.getPrototypeOf(target);
    // get the property descriptors for the class
    var props = Object.getOwnPropertyDescriptors(proto);
    for (var k in props) {
      var d = props[k];
      var { writeable, configurable, enumerable } = d;
      if (!configurable) continue;
      // bind getters and setters
      if (d.get || d.set) {
        var redef = { writeable, enumerable };
        if (d.get) {
          redef.get = d.get.bind(target);
        }
        if (d.set) {
          redef.set = d.set.bind(target);
        }
        Object.defineProperty(target, k, redef);
      }
      // bind regular methods
      if (typeof d.value == "function") {
        target[k] = target[k].bind(target);
      }
    }
  }

}

let safeData = new ProxySafeData();
let proxy = new Proxy(safeData);

console.log(proxy.privateValue); // 123
proxy.privateValue = 2;
proxy.doublePrivateValue();
console.log(proxy.privateValue); // 4

Once our methods and getter/setter functions are bound to the object instance, their this value is frozen, and the proxy can call them safely. The actual private field is still private, but we can access it and modify it through the exposed channels, the way we'd ideally want.

And for the most part, this works with Vue. Well... kind of. Let's say you wrote the following code:

<div>{{safeData.privateValue}}</div>
<button @click="safeData.privateValue++">Add one</button>
<button @click="safeData.doublePrivateValue()">Double value</button>

The first button works perfectly. You click it, the number goes up by one. But the second button is a little weird. When you click that one, nothing updates on screen--until you hit the first button again, and then suddenly the value takes a big leap, like the effect was delayed. What's the deal?

The problem is that Vue doesn't just track the surface level of a data object. Its proxy is also monitoring the dependencies of changes: if you call a method, it watches to see which properties change during that method call, and then it can use that to re-render if any of those properties were used (directly or indirectly) to generate the contents of the component. But remember that the proxy can't see into private properties. And even though we have a privateValue getter/setter that exposes its value, the private field effectively obscures the connection between doublePrivateValue() and privateValue. As a result, Vue doesn't update from the method call, because from its perspective it didn't see anything change. The side effect of that method is hidden.

Does this mean you can't use our ProxySafeData class with Vue? No, not at all. But it means we might have to wrap instances of that class with our own methods on the component that surface its values instead of directly writing them into the template, so that Vue has a way to track updates on the reactive object. For example, our Vue component options might look like this:

{
  beforeCreate() {
    this.safeData = new ProxySafeData();
  },
  data() {
    var { safeData } = this;
    return {
      value: safeData.privateValue,
      setValue(v) {
        safeData.privateValue = v;
        this.value = safeData.privateValue;
      }
      double() {
        safeData.double();
        this.value = safeData.privateValue;
      }
    }
  }
}

It's a little clunky, but it works. And just as we were able to do some metaprogramming above, this can probably be automated, or worked into a decorator if you're using TypeScript. You can probably also instantiate your class in a computed value function, or wrap it in a Vue shallowRef() as well. There are options.

Okay but why bother

I'm going to be honest, I don't really care very much about encapsulation when it comes to private fields. It doesn't really matter very much to me whether my data is perfectly safe from the prying eyes of users, or even from other code. I don't build a lot of deeply-nested class hierarchies, although I do often inherit them from the DOM.

What I do care about is expressiveness and ergonomics, and private properties do help solve some of those problems. For example, early in my career I used to write event dispatchers a lot (this was before you could just extend EventTarget). I needed a place to put the event callback table, and I think it's kind of ugly when you extend a prototype and it just sprouts new properties after instantiation, so I went through all kinds of gymnastics to put the lookup in a closure so it wasn't attached to the object.

Or, more recently, it's a really common pattern to want to have a getter or setter so that you can access something like a property, but its internal representation is different. For example, I might want to have a class that encapsulates colors, and I want to be able to treat it as a number, but also get its output in various CSS formats and access different components directly. The nice thing about private fields is that they give you a clean way to create a backing store for the public property without having a duplicate property hanging out with a _ prefix or suffix.

// bad example, but you get the idea
class Fragment {
  // rgba components
  #color = [0, 0, 0, 1];
  
  get color() {
    // return as an RGBA 32-bit value
    // yes, I know, internally they're often ABGR
    var [r,g,b,a] = this.#color;
    return (r << 24) + (g << 16) + (b << 8) + a * 255;
  }
  
  set color(v) {
    this.#color[0] = (v >> 24);
    this.#color[1] = (v >> 16) & 0xFF;
    this.#color[2] = (v >> 8) & 0xFF;
    this.#color[3] = v & 0xFF
  }

  get rgb() {
    var [ r, g, b, a] = this.#color;
    return `rgb(${r}, ${g}, ${b} / ${a})`;
  }

  set rgb(r = 0, g = 0, b = 0, a = 1) {
    this.#color = [r, g, b, a];
  }

  get hsla() {
    // insert HSLA conversion code here
  }

  // access individual components
  get r() {
    return this.#color[0]
  }

  set r(v) {
    this.#color[0] = v
  }

  // ...
}

This is pretty common on custom elements, where you may have properties that need to be checked for validity, or converted to another type, or trigger re-renders, and it's really nice to just have a private property with the same name as the public property so it's clear what's going on without being cluttered or having to remember "on this project we prefix with underscores, but on another project we use a suffix for internal data" or whatever.

I also don't write a lot of Vue, and I'm a little suspicious of running all my data through proxies (which are faster than you might be afraid of, but still orders of magnitude slower than direct property access). But I'm sympathetic to the idea that people who use a framework should still be able to have nice things. Is there a way that we can get some of the ergonomics and readability of private fields while still being proxy-compatible and not having to jump through a bunch of hoops?

The answer is actually contained in the private fields polyfill above: symbols. I think a lot of people have slept on these, because it wasn't clear what you would actually use them for in run-of-the-mill JavaScript code. But they have a lot of advantages if you're a library author who wants to clean up your class definitions:

  • They're unique keys, even if you use the same description text, so you don't have to worry about colliding with user properties or other libraries.
  • They don't require a lot of verbose code to use, just a slight change in style.
  • They can be inherited or used easily by subclasses, whereas private properties are locked to the specific class definition where they were created (although you can go through super methods).
  • You can decide the access level by keeping them in your module, exposing them through an export, or registering them globally through Symbol.for().
  • Proxies treat them like any other key, so they work perfectly with Vue and other frameworks.

For example, I might write a symbol-based class like this:

let color = Symbol();
class Fragment {

  constructor() {
    this[color] = [0, 0, 0, 1];
  }

  get rgb() {
    var [r, g, b, a] = this[color];
    return `rgb(${r}, ${g}, ${b} / ${a})`;
  }

  // ... and so on
}

If I want other people to be able to easily access the color property, I can export the color symbol, or add it as a static property on the Fragment class. It's the same number of characters as a private field identifier, it won't look weird to anyone who has used the language much, and it's perfectly inheritable for Fragment subclasses. If I use Object.defineProperty to make them non-enumerable, they won't even clutter up the console when I'm debugging.

And of course, as you start to get used to the idea of symbol keys, you start to see places where the platform already uses them, like Symbol.iterator. The point is not to dramatically state that "private fields are bad" or to defend them as good, actually. It is more that we can use a problem like this to explore what's going on under the hood, figure out how to approach it, and also learn how to use it to make ourselves more fluent--more cohesive with the browser environment, and less trying to reinvent the wheel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment