ECMAScript proposal: private prototype methods and accessors in classes

[2019-07-22] 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 blog post, we look at private methods and private accessors (getters and setters) for JavaScript classes. They are a new kind of class member that can’t be accessed outside the body of their class. To understand this post, you should be familiar with private class fields.

This feature is the subject of the ECMAScript proposal “Private methods and getter/setters for JavaScript classes” by Daniel Ehrenberg and Kevin Gibbons.

Overview: private prototype methods and accessors  

The following kinds of private prototype methods and accessors exist:

class MyClass {
  #privateOrdinaryMethod() {}
  * #privateGeneratorMethod() {}

  async #privateAsyncMethod() {}
  async * #privateAsyncGeneratorMethod() {}
  
  get #privateGetter() {}
  set #privateSetter(value) {}
}

As you can see, their names are prefixed with the same symbol # as private class fields, which indicates that they are private.

From a naming convention to true privacy  

In the following code, the name of method ._computeDist() starts with an underscore. That is a hint to clients of that class that this method is private, but it doesn’t make it truly private: It can still be accessed outside the body of class Point.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  _computeDist() {
    return Math.hypot(this.x, this.y);
  }
  dist() {
    return this._computeDist();
  }
}

assert.equal(
  new Point(4, 3).dist(),
  5 // Math.sqrt(4**2 + 3**2)
);

assert.deepEqual(
  Reflect.ownKeys(new Point().__proto__),
  ['constructor', '_computeDist', 'dist']);

In the next code fragment, we turn ._computeDist() into a private method .#computeDist():

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  #computeDist() {
    return Math.hypot(this.x, this.y);
  }
  dist() {
    return this.#computeDist();
  }
}

assert.deepEqual(
  Reflect.ownKeys(new Point().__proto__),
  ['constructor', 'dist']);

How are private methods handled in the ECMAScript specification? (advanced)  

The following code illustrates how the specification handles the private method of class Point. Private accessors are handled similarly.

{ // Begin of class scope
  class Object {
    // Maps private names to values (a list in the spec).
    // Not used in this example.
    __PrivateFieldValues__ = new Map();
    
    // Prototypes with associated private members
    __PrivateBrands__ = [];
  }
  class Point extends Object {
    static __PrivateBrand__ = Point.prototype;
    static __Fields__ = [];

    constructor(x, y) {
      super();

      // Before constructor
      InitializeInstanceElements(this, Point);

      // Body of constructor
      this.x = x;
      this.y = y;
    }
    dist() {
      PrivateBrandCheck(this, __computeDist);
      return __computeDist.__Value__.call(this);
    }
  }

  // Private name
  const __computeDist = {
    __Description__: 'computeDist',
    __Kind__: 'method',
    __Brand__: Point.prototype,
    __Value__: function () { // (A)
      return Math.hypot(this.x, this.y);
    },
  };
} // End of class scope

function InitializeInstanceElements(O, constructor) {
  if (constructor.__PrivateBrand__) {
    O.__PrivateBrands__.push(constructor.__PrivateBrand__);
  }
  const fieldRecords = constructor.__Fields__;
  for (const fieldRecord of fieldRecords) {
    O.__PrivateFieldValues__.set(fieldRecord, undefined);
  }
}

function PrivateBrandCheck(obj, privateName) {
  if (! obj.__PrivateBrands__.includes(privateName.__Brand__)) {
    throw new TypeError();
  }
}

Private names  

Similarly to private fields, there is a private name (__computeDist). This name is only accessible in the body of the class.

The private method .#computeDist() is stored in the private name, in property __Value__ (line A). Property __Brand__ indicates that the private method is associated with (but not stored in) Point.prototype. What does that mean?

  • Due to .#computeDist() not being stored in Point.prototype, it can’t be accessed outside the body of the class.
  • __computeDist.__Value__ is set up with Point.prototype as its home object. As a consequence, if you use super.prop inside .#computeDist(), the search for .prop starts inside Point.prototype.__proto__. For details, read section “Referring to superproperties in methods” in “Exploring ES6”.

Private brand checks  

In the previous section, we have seen that the brand of a private method is the prototype object it is associated with. In the internal field .__PrivateBrands__, each object records the private brands of its private methods and private accessors. This field is set up by the constructors.

Before invoking a private method or private accessor, there is always a private brand check. That check ensures that the private method is associated with one of the (original) prototypes of the receiver object (this).

Implementations  

Babel has two relevant plugins:

You can use those plugins in the Babel REPL, but you need to add them in order to activate them (bar on the left, section “Plugins”).

Further reading