ES Private Class Features: 2 Minute Standards
A number of proposals come together to provide 'private' versions of most of the class-related concepts from ES6. Let's have a #StandardsIn2Min oriented look...
The new private class features (fields and methods, both instance and static) all use a new #
to talk about private-ness. These offer compile time guarantees that only the code in this class, or instances of this class can access these things. In defining and using a private property, for example, you could write:
class Point {
#x = 0
#y = 0
constructor(x=0, y=0) {
this.#x = x
this.#y = y
}
toString() {
return `[${this.#x}, ${this.#y}]`
}
}
let p = new Point(1,2)
The result is that you can now create a Point
object which encapsulates its actual maintenance to these variables. These private properties are not accessible from the outside, as we'll see.
The #
sigil and 'private space'
The very new and perhaps most important bit to understand is that the name of private things must begin with the #
sigil, and its use is symmetrical. That is - in order to declare a class property as private, its name must begin with a #
. To access it again later, you'll use the name beginning with #
again, as in this.#x
. Attempts to access it from the outside will throw.
// x isn't #x it will log undefined..
console.log(p.x)
// #x used from an instance outside throws.
console.log(p.#x)
> Uncaught SyntaxError: Undefined private field undefined: must be declared in an enclosing class
What's subtle but important about this is that the symmetrical use of the sigil lets it be known unambiguously to both compilers and readers whether we intended to access or set something in public space, or private space. This also means that while you can choose the same readable characters, you can't actually wind up with properties with the same names in both spaces: The private ones will always include the #
This makes a lot of sense because, for example, the names that you choose for public fields should not be dictated by the internal, private fields of your super class. The following then is entirely possible and valid:
class X {
name = 'Brian'
#name = 'NotBrian
constructor() {
console.log(this.name) // Brian
console.log(this.#name) // notBrian
}
}
let x = new X()
console.log(x.name) // Brian
// (attempting to access x.#name throws)
Since the name simply begins with the sigil, it's tempting to think that you can access private fields dynamically, for example, via []
notation.
However, you can't. You also cannot access private fields of a Proxy target through the Proxy, they aren't accessible through property descriptors or Object.keys
or anything like that. This is by design as doing so, would be self-defeating, making things available outside the class as well (as you can read about in the FAQ).
Why the #, specifically?
Why this particular character was chosen was very widely discussed, and lots of options were weighed. You can read more about this in the FAQ as well, but the simplest answer is: That's the one, single character option, which we could make work without fouling up something else.
All the private things
Once you understand the above, the rest flows pretty naturally. We get private methods that are pretty self explanitory:
class Counter extends HTMLElement {
#x = 0
render() { this.innerHTML = this.#x; }
#handleClicked() {
++this.#x;
this.render();
}
constructor() {
super();
this.render();
}
connectedCallback() {
this.addEventListener('click', this.#handleClicked);
}
}
customElements.define('x-counter', Counter);
and private static fields too...
class Colors {
static #red = "#ff0000";
static #green = "#00ff00";
static #blue = "#0000ff";
}
Learn More
This is, of course, merely an 2 minute introduction. To learn a lot more details about this, including it's history, rationale about design choices, find links to the spec draft, and much more, check out the TC39 class fields proposal repository.