JS Proxy and Private Properties
The default Proxy implementation doesn’t work well with private properties, but we can fix this! I also explore other approaches to private data in JS.
What is Proxy?
The JS
Proxy
class allows you to add functions that hook into internal JS features such as
reading and writing object properties. Vue uses it to
implement its reactivity system by exposing Proxy wrapped objects to framework
users instead of the real object.
Proxy has been supported in all browsers since 2016.
const object = { x: 4, y: 10 };
const proxy = new Proxy(object, {
get(target, property, thisValue) {
if (property === "x") {
return -target.x;
}
// The `Reflect` class contains the default
// implementations of the `Proxy` hooks,
// for your convenience
return Reflect.get(target, property, thisValue);
},
});
console.log(object.y); //=> 10
console.log(object.x); //=> 4
console.log(proxy.y); //=> 10
console.log(proxy.x); //=> -4
You can implement many powerful patterns with Proxy, such as objects that
throw errors when you try to access missing keys, or listen for modifications to
the properties of an object without using
setters.
What are private properties?
Private properties have been supported in all browsers since 2021.
Private properties are object properties that can only be accessed from methods
defined inside the class declaration of an object’s constructor. These are
usually combined with
setters and getters
to define custom behavior when reading/writing object properties.
class Thing {
#x;
constructor(value) {
this.#x = value;
}
set x(value) {
console.log("SET x", value);
this.#x = value;
}
get x() {
console.log("GET x");
return this.#x;
}
}
const thing = new Thing("secret");
thing.x;
//=> "GET x"
console.log(thing.x);
//=> "GET x"
//=> "secret"
console.log(thing.#x);
// SyntaxError: can't access private property
// outside of class definition
thing.x = "new_secret";
//=> "SET x new_secret"
console.log(thing.x);
//=> "GET x"
//=> "new_secret"
What happens when you combine them?
You might expect that if you make a Proxy but don’t add any hook function,
that the new proxy would behave more or less identicaly to the original object.
This is mostly true, but unfortunately methods that reference private properties will crash by default.
class CoolValue {
#value;
constructor(value) {
this.#value = value;
}
get value() {
return this.#value;
}
logValue() {
console.log(this.#value);
}
}
const v = new CoolValue("hello");
console.log(v.value);
//=> "hello"
const p = new Proxy(v, {});
console.log(p.value);
// TypeError: can't access private field or method:
// object is not the right class
p.logValue();
// TypeError: can't access private field or method:
// object is not the right class
We can fix this problem by defining a different get hook for the Proxy.
const p = new Proxy(v, {
get(target, prop, thisValue) {
// Ignore `thisValue`, which is the proxy itself.
// `target` is the original object known here as `v`.
// This could also be written as `target[prop]`.
const value = Reflect.get(target, prop, target);
// If the value is a function, we need to bind the function
// to use the correct `this` value of `target`,
// the underlying object. Otherwise `this` will be
// set to `thisValue`, which is the proxy.
// The proxy doesn't have access to the
// private properties of the class.
if (typeof value === "function") {
return value.bind(target);
}
return value;
},
});
console.log(p.value);
p.logValue();
What about WeakMap?
WeakMap
can be used to simulate private data, but the behavior will be even worse.
Rather than throwing errors about not having access to private data, the
underlying map will simply return undefined with no explanation.
const $value = new WeakMap();
class CoolValue {
constructor(value) {
$value.set(this, value);
}
get value() {
return $value.get(this);
}
logValue() {
console.log($value.get(this));
}
}
const v = new CoolValue("hello");
console.log(v.value);
//=> "hello"
const p = new Proxy(v, {});
console.log(p.value);
//=> undefined
p.logValue();
//=> undefined
What about Symbol keys?
Using secret Symbol values as the keys for “private” values seems to work
really well. It’s a bit annoying compared to private properties, and it can
circumvented if you try really hard, but these properties are invisible to most
JS methods (e.g. Object.keys or JSON.stringify).
// The Symbol name is optional, but it's
// good practice to provide one for
// debugging purposes. Otherwise every Symbol key
// will just look like Symbol() in the
// Developer Tools when inspecting an object.
const $value = Symbol("CoolValue.value");
class CoolValue {
constructor(value) {
this[$value] = value;
}
get value() {
return this[$value];
}
logValue() {
console.log(this[$value]);
}
}
const v = new CoolValue("hello");
console.log(v.value);
//=> "hello"
const p = new Proxy(v, {});
console.log(p.value);
//=> "hello"
p.logValue();
//=> "hello"
In fact, JS already uses Symbol for pseudo-private properties like
Symbol.toStringTag
and
Symbol.iterator.
It seems like this feature was added primarily to allow multiple types of APIs
to exist on an object, without requiring names to be unique (all Symbol values
are not equal to each other—like objects—even if they have the same
display name).
Inspired by Lea Verou
I was originally inspired by Lea Verou’s post JS private class fields considered harmful.
I wanted to find if there was a workaround for the problem she described. I’m
not a seasoned Vue developer like she is, so I’ll assume that I’m still missing
some corner case of this solution that makes it not work at least for how Vue
wants to use Proxy. But I’m glad to see you can fix it in the simple case, at
least.
Combinations of features can be surprising
It’s never a pleasant feeling when two features of a language don’t combine well
like Proxy and private properties. I can’t help but feel that private
properties should’ve just been syntax sugar over a Symbol made behind the
scenes anyway. Maybe I’m just a minimalist, but I feel like building on top of
existing features is good. But I also remember programming in JS before ES5 came
out, and frankly it was still a pretty good language even back then (hot take).
Yes, you can use Object.getOwnPropertySymbols to enumerate the symbols for an
object, but this is nearly impossible to do on accident. If I had the time and
energy, maybe I could browse the GitHub discussions for why the private property
proposal went with true privacy instead of just being great syntax sugar over
nearly-private fields using Sybmol keys.
Going forward with private properties
All of this mess around Proxy has me feeling a bit awkward about whether or
not I should keep using private properties in my code in the future. I’ve
written already about
developing with web components
wherein I use private properties extensively. I have no need for Proxy in that
code, and the workaround I mentioned earlier could still be employed if I did.
I won’t deny that there’s a certain elegance to the idea behind hidden Symbol
properties on objects, but the syntactic awkwardness will probably prevent me
from using them in most code I work on.