This blog post is part of a series on new members in bodies of class definitions:
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.
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.
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']);
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();
}
}
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?
.#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”.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
).
Babel has two relevant plugins:
@babel/plugin-proposal-class-properties
@babel/plugin-proposal-private-methods
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”).