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.