ECMAScript proposal: private class fields

[2019-07-17] dev, javascript, es proposal, js classes
(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 prototype methods and getter/setters in classes
  4. Private static methods and getter/setters in classes

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:

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: '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.

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: '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()),
  []);

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.

{ // 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:

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

“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;
  }
}

Whenever an expression such as other.value appears in the body of MyClass, JavaScript has to decide:

  • Is .value a public property?
  • Is .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:

  1. .value is always interpreted as a private field.
  2. JavaScript decides at runtime:
    • If other is an instance of MyClass, then .value is interpreted as a private field.
    • Otherwise .value is interpreted as a public property.

Both options have downsides:

  • With option (1), we can’t use .value as a public property, anymore – for any object.
  • With option (2), there is a performance penalty.

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.

Further reading