This blog post is part of a series on new members in bodies of class definitions:
In this post, we look at private fields, a new kind of private slot in instances and classes. This feature is specified by two ECMAScript proposals:
Private fields are a new kind of data slot that is different from properties. They can only be accessed directly inside the body of the class in which they are declared.
class MyClass {
// Declare and initialize
static #privateStaticField = 1;
static getPrivateStaticField() {
return MyClass.#privateStaticField; // (A)
}
}
assert.throws(
() => eval('MyClass.#privateStaticField'),
{
name: 'SyntaxError',
message: 'Private field \'#privateStaticField\'' +
' must be declared in an enclosing class',
}
);
assert.equal(MyClass.getPrivateStaticField(), 1);
Tip: Never use this
to access a private static field, always use the direct class name (as in line A). Why is explained later in this post.
Using private fields with initializers (equal signs followed by values):
class MyClass {
// Declare and initialize
#privateInstanceField = 2;
getPrivateInstanceField() {
return this.#privateInstanceField;
}
}
assert.throws(
() => eval('new MyClass().#privateInstanceField'),
{
name: 'SyntaxError',
message: 'Private field \'#privateInstanceField\'' +
' must be declared in an enclosing class',
}
);
assert.equal(new MyClass().getPrivateInstanceField(), 2);
Using private instance fields without initializers:
class DataStore {
#data; // must be declared
constructor(data) {
this.#data = data;
}
getData() {
return this.#data;
}
}
assert.deepEqual(
Reflect.ownKeys(new DataStore()),
[]);
A common technique for keeping data private in JavaScript is to prefix property names with underscores. In this section, we’ll start with code that uses this technique and then change it, so that it uses private instance fields.
class Countdown {
constructor(counter, action) {
this._counter = counter; // private
this._action = action; // private
}
dec() {
if (this._counter < 1) return;
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
// The data is not really private:
assert.deepEqual(
Reflect.ownKeys(new Countdown(5, () => {})),
['_counter', '_action']);
This technique doesn’t give us any protection; it merely suggests to people using this class: Don’t touch these properties, they are considered private.
The main benefit of this technique is that it is convenient. With private fields, we don’t lose the convenience and gain true privacy.
We can switch from underscores to private fields 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();
}
}
}
// The data is now private:
assert.deepEqual(
Reflect.ownKeys(new Countdown(5, () => {})),
[]);
For example, instance methods can access private static fields:
class MyClass {
static #privateStaticField = 1;
getPrivateFieldOfClass(theClass) {
return theClass.#privateStaticField;
}
}
assert.equal(
new MyClass().getPrivateFieldOfClass(MyClass), 1);
And static methods can access private instance fields:
class MyClass {
#privateInstanceField = 2;
static getPrivateFieldOfInstance(theInstance) {
return theInstance.#privateInstanceField;
}
}
assert.equal(
MyClass.getPrivateFieldOfInstance(new MyClass()), 2);
The remaining sections cover advanced aspects of private fields.
In the ECMAScript specification, private fields are managed via a data structure that is attached to objects. That is, private fields are roughly handled as follows.
{ // Begin of class scope
// Private names
const __counter = {
__Description__: 'counter',
__Kind__: 'field',
};
const __action = {
__Description__: 'action',
__Kind__: 'field',
};
class Object_ {
// Maps private names to values (a list in the spec).
__PrivateFieldValues__ = new Map();
}
class Countdown extends Object_ {
static __Fields__ = [__counter, __action];
constructor(counter, action) {
super();
// Setup before constructor
InitializeInstanceElements(this, Countdown);
// Code inside constructor
this.__PrivateFieldValues__.set(__counter, counter);
this.__PrivateFieldValues__.set(__action, action);
}
dec() {
if (this.__PrivateFieldValues__.get(__counter) < 1) return;
this.__PrivateFieldValues__.set(
__counter, this.__PrivateFieldValues__.get(__counter) - 1);
if (this.__PrivateFieldValues__.get(__counter) === 0) {
this.__PrivateFieldValues__.get(__action)();
}
}
}
} // End of class scope
function InitializeInstanceElements(O, constructor) {
const fields = constructor.__Fields__;
for (const fieldRecord of fields) {
O.__PrivateFieldValues__.set(fieldRecord, undefined);
}
}
Comments:
.__PrivateFieldValues__
only being accessible to the engine, we don’t need to take measures to protect it.Map
made the example easier to understand.).__PrivateFieldValues__
then the engine throws a TypeError
.Consequences:
.#counter
and .#action
if you are inside the body of class Countdown
– because you only have access to the private names there.Sometimes you evaluate the same class definition multiple times. That’s what classFactory()
does in the following example:
const classFactory = () => class {
static getValue(instance) {
return instance.#value;
}
#value;
constructor(value) {
this.#value = value;
}
};
const Class1 = classFactory();
const Class2 = classFactory();
const inst1 = new Class1(1);
const inst2 = new Class2(2);
The private name #value
is created freshly each time. Therefore, Class1.getValue()
works for inst1
, but not for inst2
:
assert.equal(Class1.getValue(inst1), 1);
assert.throws(
() => Class1.getValue(inst2),
TypeError);
this
to access private static fields You can use this
to access public static fields, but you shouldn’t use it to access private static fields.
this
and public static fields Consider the following code:
class SuperClass {
static publicData = 1;
static getPublicViaThis() {
return this.publicData;
}
}
class SubClass extends SuperClass {
}
Public static fields are properties. If we make the method call:
assert.equal(SuperClass.getPublicViaThis(), 1);
then this
points to SuperClass
and everything works as expected. We can also invoke .getPublicViaThis()
via the subclass:
assert.equal(SubClass.getPublicViaThis(), 1);
SubClass
inherits .getPublicViaThis()
, this
points to SubClass
and things continue to work, because SubClass
also inherits the property .publicData
.
(As an aside, setting .publicData
in this case would create a new property inside SubClass
that non-destructively overrides the property in SuperClass
.)
this
and private static fields Consider the following code:
class SuperClass {
static #privateData = 2;
static getPrivateDataViaThis() {
return this.#privateData;
}
static getPrivateDataViaClassName() {
return SuperClass.#privateData;
}
}
class SubClass extends SuperClass {
}
Invoking .getPrivateDataViaThis()
via SuperClass
works, because this
points to SuperClass
:
assert.equal(SuperClass.getPrivateDataViaThis(), 2);
However, invoking .getPrivateDataViaThis()
via SubClass
does not work, because this
now points to SubClass
and SubClass
has no private static field .#privateData
:
assert.throws(
() => SubClass.getPrivateDataViaThis(),
{
name: 'TypeError',
message: 'Cannot read private member #privateData from an object whose class did not declare it',
}
);
The work-around is to accesss .#privateData
directly, via SuperClass
:
assert.equal(SubClass.getPrivateDataViaClassName(), 2);
Sometimes, we want certain entities to be “friends” of a class. Such friends should have access to the private data of the class. In the following code, the function getCounter()
is a friend of the class Countdown
. We use WeakMaps to make data private, which allows Countdown
to let friends access that data.
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)();
}
}
}
function getCounter(countdown) {
return counter.get(countdown);
}
It is easy to control who has access to the private data: If they have access to _counter
and _action
, they have access to the private data. If we put the previous code fragment inside a module then the data is private within the whole module.
For more information on this technique, consult Sect. “Keeping private data in WeakMaps” in “JavaScript for impatient programmers”. It also works for sharing private data between a superclass and subclasses (“protected” visibility).
#
? Why not declare private fields via private
? In principle, private fields could be handled as follows:
class MyClass {
private value;
compare(other) {
return this.value === other.value;
}
}
Whenever an expression such as other.value
appears in the body of MyClass
, JavaScript has to decide:
.value
a public property?.value
a private field?Statically, JavaScript doesn’t know if the declaration private value
applies to other
(due to it being an instance of MyClass
) or not. That leaves two options for making the decision:
.value
is always interpreted as a private field.other
is an instance of MyClass
, then .value
is interpreted as a private field..value
is interpreted as a public property.Both options have downsides:
.value
as a public property, anymore – for any object.That’s why the name prefix #
was introduced. The decision is now easy: If we use #
, we want to access a private field. If we don’t, we want to access a public property.
private
works for statically typed languages (such as TypeScript) because they know at compile time if other
is an instance of MyClass
and can then treat .value
as private or public.
WeakMap
)” in “JavaScript for impatient programmers”.