The ECMAScript proposal “Class Fields” by Daniel Ehrenberg and Jeff Morrison is currently at stage 3. This blog post explains how it works.
Field declarations:
class MyClass {
instanceProperty = 0;
static staticProperty = 0;
}
Private fields have names starting with a #
and are only visible within the body of the class:
class MyClass {
#foo; // must be declared
constructor(foo) {
this.#foo = foo;
}
incFoo() {
this.#foo++;
}
}
With this proposal, objects now have two kinds of fields:
Fields can be configured as follows:
static
Location | Visibility/name | |
---|---|---|
foo; |
instance | public |
#foo; |
instance | private |
['f'+'oo']; |
instance | computed |
static foo; |
static | public |
static #foo; |
static | private |
static ['f'+'oo']; |
static | computed |
With an initializer, you create a property and assign it a value at the same time. In the following code, = 0
is an initializer:
class MyClass {
x = 0;
y = 0;
}
This class is equivalent to:
class MyClass {
constructor() {
this.x = 0;
this.y = 0;
}
}
class MyClass {
foo = console.log('initializer');
constructor() {
console.log('constructor');
}
}
new MyClass();
// Output:
// initializer
// constructor
Without any prefix, a declaration creates an instance field:
class MyClass {
foo = 123;
}
console.log(new MyClass().foo); // 123
console.log(Reflect.ownKeys(new MyClass()));
// ['foo']
Declarations with the prefix static
create fields for classes:
class MyClass {
static foo = 123;
}
console.log(MyClass.foo); // 123
console.log(Reflect.ownKeys(MyClass)); // ['foo']
// ['length', 'name', 'prototype', 'foo']
MyClass
has the properties length
and name
, because it is also a function.
In ES6 and later, you can already implement two kinds of privacy:
const _counter = Symbol('counter');
const _action = Symbol('action');
class Countdown {
constructor(counter, action) {
this[_counter] = counter;
this[_action] = action;
}
dec() {
if (this[_counter] < 1) return;
this[_counter]--;
if (this[_counter] === 0) {
this[_action]();
}
}
}
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
Private fields are basically a more convenient way of doing hard privacy.
Another common technique is to indicate which properties are considered private by prefixing their names with underscores:
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}
dec() {
if (this._counter < 1) return;
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
This technique doesn’t give you any protection, but it is more convenient than using symbols or WeakMaps.
Such code can be changed to use the new private field feature in two steps:
class Countdown {
#counter;
#action;
constructor(counter, action) {
this.#counter = counter;
this.#action = action;
}
dec() {
if (this.#counter < 1) return;
this.#counter--;
if (this.#counter === 0) {
this.#action();
}
}
}
Countdown
does not have any instance properties:
const countdown = new Countdown(5, () => {});
Reflect.ownKeys(countdown); // []
In the spec, private fields are managed via a data structure that is attached to objects. That is, private fields are roughly handled as follows.
{
const _counter = Symbol();
const _action = Symbol();
class Countdown {
__PrivateFieldValues__ = {
[_counter]: undefined,
[_action]: undefined,
};
constructor(counter, action) {
this.__PrivateFieldValues__[_counter] = counter;
this.__PrivateFieldValues__[_action] = action;
}
···
}
}
A consequence of this approach is that you can only access private fields if you are inside the body of a class; access to this
does not give you access to private data. In other words, you need to know the right symbol to access the data (__PrivateFieldValues__
in the example is not fully protected, but the corresponding data structure in the spec is).
More information in the spec: Sect. “Private Names and references”
Two elements of classes cannot yet be private:
An upcoming proposal that fills this gap is currently at stage 2.
If you switch on stages 2+ in the Babel REPL, you can play with class fields (however, private fields are not supported, yet).
#
? Why not declare private fields via private
? First, the #
clearly indicates that private fields are not properties. They are a completely different mechanism.
Second, if you declared private fields via private
and if they looked like normal properties, property lookup would become more complicated:
class MyClass {
private name;
compare(other) {
return this.name === other.name;
}
}
If other
is a direct(!) instance of MyClass
, other.name
is a private field. Otherwise, it refers to a normal property.