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 methods and accessors in JavaScript 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 methods and getter/setters for JavaScript classes

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


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.

Overview: private methods and accessors  

The following kinds of private 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.

{
  // Private name
  const __computeDist = {
    __Description__: 'computeDist',
    __Kind__: 'method',
    __Brand__: Point.prototype,
    __Value__: function () { // (A)
      return Math.hypot(this.x, this.y);
    },
  };
  class Object {
    // Maps private names to values
    // (not used in this example)
    __PrivateFieldValues__ = new Map();
    
    // Prototypes with associated private members
    __PrivateBrands__ = new Set();
  }
  class Point extends Object {
    static __PrivateBrand__ = Point.prototype;
    constructor(x, y) {
      this.__PrivateBrands__.add(Point.__PrivateBrand__);
      this.x = x;
      this.y = y;
    }
    dist() {
      PrivateBrandCheck(this, __computeDist);
      __computeDist.__Value__.call(this);
    }
  }
}
function PrivateBrandCheck(obj, privateName) {
  if (! obj.__PrivateBrands__.has(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