Portrait Dr. Axel Rauschmayer
Dr. Axel Rauschmayer
Homepage | Twitter
Cover of book “Exploring ES6”
Book, exercises, quizzes
(free to read online)
Logo of newsletter “ES.next news”
Newsletter (free)
Cover of book “JavaScript for impatient programmers”
Book (free online)

ES proposal: private class fields

(Ad, please don’t block)

This blog post is part of a series on new members in bodies of class definitions:

  1. Public class fields
  2. Private class fields
  3. Private methods and getter/setters for JavaScript classes

This series replaces 2ality’s prior blog post on fields.


In this post, we look at private fields, a new kind of private slot in instances and classes. This feature is part of the ES proposal “Class field declarations for JavaScript” by Daniel Ehrenberg and Jeff Morrison.

Overview  

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.

Private static fields  

class MyClass {
  // Declare and initialize
  static #privateStaticField = 1;
  static getPrivateStaticField() {
    return MyClass.#privateStaticField; // (A)
  }
}
assert.throws(
  () => eval('MyClass.#privateStaticField'),
  {
    name: 'SyntaxError',
    message: 'Undefined private field undefined:' +
      ' 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.

Private instance fields  

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: 'Undefined private field undefined:' +
      ' 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()),
  []);

From underscores to private instance fields  

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.

Starting with underscores  

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.

Switching to private instance fields  

We can switch from underscores to private fields in two steps:

  1. We replace each underscore with a hash symbol.
  2. We declare all private fields at the beginning of the class.
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, () => {})),
  []);

All code inside the body of a class can access all private fields  

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);

(Advanced)  

The remaining sections cover advanced aspects of private fields.

How are private fields managed under the hood?  

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.

{
  // Private names
  const __counter = {
    __Description__: 'counter',
    __Kind__: 'field',
  };
  const __action = {
    __Description__: 'action',
    __Kind__: 'field',
  };

  class Object_ {
    // Maps private names to values
    __PrivateFieldValues__ = new Map();
  }

  class Countdown extends Object_ {
    constructor(counter, action) {
      super();
      // Setup before constructor
      this.__PrivateFieldValues__.set(__counter, undefined);
      this.__PrivateFieldValues__.set(__action, undefined);
      
      // 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)();
      }
    }
  }
}

Comments:

  • In this example, everything that has two underscores is metadata that is managed by the JavaScript engine and only accessible to it.
  • Private names are unique keys. They are only accessible inside the body of the class.
  • Private field values is a dictionary that maps private names to values. Each instance with private fields has such a dictionary.
    • Due to .__PrivateFieldValues__ only being accessible to the engine, we don’t need to take measures to protect it.
    • (As an aside, the specification stores private field values in lists. However, using a Map made the example easier to understand.)
    • After setup, if you use a private name that is not already a key in .__PrivateFieldValues__ then the engine throws a TypeError.

Consequences:

  • You can only access the private data stored in .#counter and .#action if you are inside the body of class Countdown – because you only have access to the private names there.
  • Private fields are not accessible in subclasses.

Each evaluation of a class definition produces new private names  

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);

Pitfall: Using 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 getPrivateViaThis() {
    return this.#privateData;
  }
  static getPrivateViaClassName() {
    return SuperClass.#privateData;
  }
}
class SubClass extends SuperClass {
}

Invoking .getPrivateViaThis() via SuperClass works, because this points to SuperClass:

assert.equal(SuperClass.getPrivateViaThis(), 2);

However, invoking .getPrivateViaThis() via SubClass does not work, because this now points to SubClass and SubClass has no private static field .#privateData:

assert.throws(
  () => SubClass.getPrivateViaThis(),
  {
    name: 'TypeError',
    message: 'Read of private field #privateData from' +
      ' an object which did not contain the field',
  }
);

The work-around is to accesss .#privateData directly, via SuperClass:

assert.equal(SubClass.getPrivateViaClassName(), 2);

“Friend” and “protected” privacy  

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).

FAQ  

Why the #? 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;
  }
}

But then we couldn’t use the property name value inside the class body anymore – it would always be interpreted as a private name.

Statically typed languages such as TypeScript have more flexibility here: They know at compile time if other is an instance of MyClass and can then treat .value as private or not.

Further reading