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.
There are three popular ways of creating private class properties in ES2015.
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}
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.
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 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.
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.
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
).
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.
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Learn JavaScript - Become a Zero to Hero
☞ Javascript Project Tutorial: Budget App
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch