How to Create Private Class Properties in JavaScript

How to Create Private Class Properties in JavaScript

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.

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.

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.

Thank you for reading ! If you liked this post, share it with all of your programming buddies!

javascript programming js javascriptips wevdev

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

Vue.js 3 is Future-Oriented Programming, Don't miss !

If you are interested in Vue.js, you probably know about the 3rd version of this framework. This article is aimed at people who have at least some background in JavaScript and Vue.

Creating a precise countdown with Vanilla JS

On a recent technical interview I had for a big tech company, in one of the steps of the process I was asked the following: Create a countdown from 1:30 to zero using plain javascript and HTML, and that’s all. Creating a precise countdown with Vanilla JS

Quantum JavaScript - Programming

Q is a quantum circuit simulator, drag-and-drop circuit editor, and powerful JavaScript library that runs right in your Web browser.

Learning JavaScript: Development Environments for JavaScript Programming

One of the nice things about learning JavaScript these days is that there is a plethora of choices for writing and running JavaScript code. In this article, I’m going to describe a few of these environments and show you the environment I’ll be using in this series of articles.