Doing it Right: Private Class Properties in JavaScript

A look at the class fields proposals and the existing workarounds

Image by Chris Sansbury from Pixabay

The class syntax has been one of the most popular features introduced in ES2015. However, the lack of native support for private properties has hindered it from reaching its full potential. Although the JavaScript community has come up with various patterns and workarounds to implement similar functionality, they’re still not as easy-to-use as a native implementation.

Fortunately, there’s a proposal that would simplify the process of creating private properties. The proposal, called class fields, is currently at stage 3, which means its syntax, semantics, and API are completed. Chrome 74+ is the only browser that fully supports class fields at the moment. But you can use a transpiler like Babel to implement them on older platforms.

Compared to existing workarounds, class fields are much easier to use. However, it’s still important to be aware of how other patterns work. When working on a project, you may have to work on another programmer’s code that’s not up-to-date. In such a situation, you need to know how the code works and how to upgrade it.

Therefore, before delving into the class fields, we take a look at existing workarounds and their shortcomings.

Tip: Share and reuse utility and UI components

Use Bit to easily publish, install and update small individual modules across different JavaScript (NodeJS, React, Angular, Vue, etc.) projects. Don’t install entire libraries and don’t copy-paste code, when you can build faster with small reusable modules. Take a look.

Example: React components in Bit’s cloud

Private Properties in ES2015

There are three popular ways of creating private class properties in ES2015.

A Leading Underscore

Using a leading underscore ( _ ) is a common convention to show that a property is private. Although such a property isn’t really private, it’s generally adequate to signal to a programmer that the property shouldn’t be modified. Let’s look at an example:

class SmallRectangle {

constructor() {

this._width = 20;

this._height = 10;

} get dimension() {

return {

width: this._width,

height: this._height

};

} increaseSize() {

this._width++;

this._height++;

}

}

This class has a constructor that creates two instance properties: _width and _height , both of which are considered private. But because these properties can be overwritten accidentally, this approach is not reliable for creating private class properties:

const rectangle = new SmallRectangle(); console.log(rectangle.dimension); // => {width: 20, height: 10} rectangle._width = 0;

rectangle._height = 50; console.log(rectangle.dimension); // => {width: 0, height: 50}

Scoped Variables

A safer way to make private members is to declare variables inside the class constructor instead of attaching them to the new instance being created:

class SmallRectangle {

constructor() {

let width = 20;

let height = 10; this.getDimension = () => {

return {width: width, height: height};

}; this.increaseSize = () => {

width++;

height++;

};

}

} const rectangle = new SmallRectangle(); console.log(rectangle.getDimension()); // => {width: 20, height: 10} // here we cannot access height and width

console.log(rectangle.height); // => undefined

console.log(rectangle.width); // => undefined

In this code, the scope of the constructor function is used to store private variables. Since the scope is private, you can truly hide variables that you don’t want to be publicly accessible. But it’s still not an ideal solution: getDimension() and increaseSize() are now created on every instance of SmallRectangle .

Unlike methods defined on the prototype, which are shared, methods defined on instances take up separate space in the environment’s memory. This won’t be a problem if your program creates a small number of instances, but complex programs that require hundreds of instances will be slowed down.

Scoped WeakMaps

Compared to a Map , a WeakMap has a more limited feature set. Because it doesn’t provide any method to iterate over the collection, the only way to access a value is to use the reference that points to the value’s key.

Additionally, only objects can be used as keys. But in exchange for these limitations, the keys in a WeakMap are weakly referenced, meaning that the WeakMap automatically removes the values when the object keys are garbage collected. This makes WeakMaps very useful for creating private variables. Here’s an example:

const SmallRectangle = (() => {

const pvtWidth = new WeakMap();

const pvtHeight = new WeakMap(); class SmallRectangle {

constructor(name) {

pvtWidth.set(this, 20); // private

pvtHeight.set(this, 10); // private

} get dimension() {

return {

width: pvtWidth.get(this),

height: pvtHeight.get(this)

};

} increaseSize() {

pvtWidth.set(this, pvtWidth.get(this) + 1);

pvtHeight.set(this, pvtHeight.get(this) + 1);

}

} return SmallRectangle;

})(); const rectangle = new SmallRectangle(); // here we can access public properties but not private ones

console.log(rectangle.width); // => undefined

console.log(rectangle.height); // => undefined

console.log(rectangle.dimension); // => {width: 20, height: 10}

This technique has all the benefits of the previous approach but doesn’t incur a performance penalty. That being said, the process is unnecessarily complicated, given many other languages provide a native API that’s very simple to use.

Class Fields Proposal

Class fields are designed to simplify the process of creating private class properties. The syntax is very simple: add a # before the name of a property, and it will become private.

Using private fields, we can rewrite the previous example like this:

class SmallRectangle {

#width = 20;

#height = 10;



get dimension() {

return {width: this.#width, height: this.#height};

} increaseSize() {

this.#width++;

this.#height++;

}

} const rectangle = new SmallRectangle(); console.log(rectangle.dimension); // => {width: 20, height: 10} rectangle.#width = 0; // => SyntaxError

rrectangle.#height = 50; // => SyntaxError

Note that you also have to use # when you want to access a private field. As of this writing, it’s not possible to use this syntax to define private methods and accessors. There’s another stage 3 proposal, however, that aims to fix that. Here’s how it would work:

class SmallRectangle {

#width = 20;

#height = 10;



// a private getter

get #dimension() {

return {width: this.#width, height: this.#height};

}



// a private method

#increaseSize() {

this.#width++;

this.#height++;

}

}

It’s important to keep in mind that the name of private fields cannot be computed. Attempting to do so throws a SyntaxError :

const props = ['width', 'height']; class SmallRectangle {

#[props[1]] = 20; // => SyntaxError

#[props[0]] = 10; // => SyntaxError

}

To simplify the class definition, the proposal also provides a new way to create public properties. Now you can declare public properties directly in the class body. So, a constructor function isn’t required anymore. For example:

class SmallRectangle {

width = 20; // a public field

height = 10; // another public field get dimension() {

return {width: this.width, height: this.height};

}

} const rectangle = new SmallRectangle(); rectangle.width = 100;

rectangle.height = 50; console.log(rectangle.dimension); // => {width: 100, height: 50}

As you can see, public fields are created in a similar way to private fields except that they don’t have a hashtag.

Inheritance

Class fields also provide a simpler subclassing. Consider the following example:

class Person {

constructor(param1, param2) {

this.firstName = param1;

this.lastName = param2;

}

} class Student extends Person {

constructor(param1, param2) {

super(param1, param2);

this.schoolName = 'Princeton';

}

}

This code is very straightforward. The Student class inherits from Person and adds an additional instance property. With public fields, creating a subclass can be simplified like this:

class Person {

constructor(param1, param2) {

this.firstName = param1;

this.lastName = param2;

}

} class Student extends Person {

schoolName = 'Princeton'; // a public class field

} const student = new Student('John', 'Smith'); console.log(student.firstName); // => John

console.log(student.lastName); // => Smith

console.log(student.schoolName); // => Princeton

Therefore, it’s no longer necessary to call super() to execute the constructor of the base class or put the code in the constructor function.

Static Class Fields

You can make a class field static by preceding it with the static keyword. A static class field is called on the class itself, as shown in this example:

class Car {

static #topSpeed = 200; // convert mile to kilometer

convertMiToKm(mile) {

const km = mile * 1.609344;

return km;

} getTopSpeedInKm() {

return this.convertMiToKm(Car.#topSpeed);

}

} const myCar = new Car;

myCar.getTopSpeedInKm(); // => 321.8688

Keep in mind that static class fields are linked to the class itself, not instances. To access a static field, you must use the name of the class (instead of this ).

Conclusion

In this post, we’ve taken a good look at the new and existing ways of creating private class properties in JavaScript. We learned about the shortcomings of existing methods and saw how the class fields proposal tries to fix and simplify the process.

As of this writing, you can use class fields in Chrome 74+ and Node.js 12 without having to use flags or transpilers. Firefox 69 supports public class fields but not private once. Wider browser support is expected soon. Until then, you can use babel to implement the feature on web browsers that do not support it yet.

Learn More