• Private Variables in JavaScript
  • Marcus Noble
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Noah Gao
  • Proofread by: Old Professor Ryouaki

Private variables in JavaScript

JavaScript has been improved a lot lately, with new syntax and functionality being added all the time. But some things don’t change, everything is still an object, almost everything can be changed at run time, and there is no notion of public or private properties. But there are a few tricks you can use to change this. In this article, I show you the various ways you can implement private variables.

In 2015, JavaScript got classes, and programmers from more traditional C languages like Java and C# will be more familiar with this way of manipulating objects. But obviously, these classes aren’t what you’re used to — their properties don’t have modifiers to control access, and all properties need to be defined in functions.

So how can we protect data that should not be altered at run time? Let’s look at some of the options.

I’ll use an example class for building shapes repeatedly throughout this article. Its width and height can only be set at initialization, providing a property to get the area. For more information about the get keyword used in these examples, see my previous articles Getters and Setters.

Naming conventions

The first and most sophisticated approach is to use a specific naming convention to indicate that properties should be treated as private. Attribute names are usually prefixed with an underscore (for example, _count). This does not really prevent the variable from being accessed or modified, but rather relies on mutual understanding between developers that the variable should be considered restricted access.

class Shape {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }
  get area() {
    return this._width * this._height; }}const square = new Shape(10.10);
console.log(square.area);    / / 100
console.log(square._width);  / / 10
Copy the code

WeakMap

To be a little more restrictive, you can use WeakMap to store all private values. This still does not block access to the data, but it separates the private values from the objects that the user can manipulate. For this technique, we set the Key of WeakMap to the instance of the object that the private property belongs to, and we use a function (we call it internal) to create or return an object where all the properties will be stored. The advantage of this technique is that it does not expose the instance’s private properties when traversing properties or when executing json.stringify, but it relies on a WeakMap variable that is placed outside the class and can be accessed and manipulated.

const map = new WeakMap(a);// Create an object that stores private variables in each instance
const internal = obj= > {
  if(! map.has(obj)) { map.set(obj, {}); }return map.get(obj);
}

class Shape {
  constructor(width, height) {
    internal(this).width = width;
    internal(this).height = height;
  }
  get area() {
    return internal(this).width * internal(this).height; }}const square = new Shape(10.10);
console.log(square.area);      / / 100
console.log(map.get(square));  // { height: 100, width: 100 }
Copy the code

Symbol

Symbol is implemented in a very similar way to WeakMap. Here, we can create properties on the instance by using Symbol as the key. This prevents the property from being visible when traversing or using json.stringify. However, this technique requires the creation of a Symbol for each private attribute. If you can access the Symbol outside the class, you can still get the private property.

const widthSymbol = Symbol('width');
const heightSymbol = Symbol('height');

class Shape {
  constructor(width, height) {
    this[widthSymbol] = width;
    this[heightSymbol] = height;
  }
  get area() {
    return this[widthSymbol] * this[heightSymbol]; }}const square = new Shape(10.10);
console.log(square.area);         / / 100
console.log(square.widthSymbol);  // undefined
console.log(square[widthSymbol]); / / 10
Copy the code

closure

All of the techniques shown so far still allow private attributes to be accessed from outside the class, and closures provide a way around that. Closures can be used with WeakMap or Symbol if you wish, but this approach can also be used with standard JavaScript objects. The idea behind closures is to encapsulate data within the scope of the function created when called, but return the result of the function from within, making the scope inaccessible from the outside.

function Shape() {
  // Set of private variables
  const this$= {};class Shape {
    constructor(width, height) {
      this$.width = width;
      this$.height = height;
    }

    get area() {
      return this$.width * this$.height; }}return newShape(... arguments); }const square = new Shape(10.10);
console.log(square.area);  / / 100
console.log(square.width); // undefined
Copy the code

One small problem with this technique is that we now have two different Shapes. The code will call and interact with the external Shape, but the returned instance will be the internal Shape. This may not be a big deal in most cases, but can cause the Square Instanceof Shape expression to return false, which can be a problem in your code.

The solution to this problem is to set the external Shape to return the prototype of the instance:

return Object.setPrototypeOf(newShape(... arguments),this);
Copy the code

Unfortunately, this is not enough, and just updating the line will now treat square.area as undefined. This is because the get keyword works behind the scenes. We can solve this problem by manually specifying the getter in the constructor.

function Shape() {
  // Set of private variables
  const this$= {};class Shape {
    constructor(width, height) {
      this$.width = width;
      this$.height = height;

      Object.defineProperty(this.'area', {
        get: function() {
          return this$.width * this$.height; }}); }}return Object.setPrototypeOf(newShape(... arguments),this);
}

const square = new Shape(10.10);
console.log(square.area);             / / 100
console.log(square.width);            // undefined
console.log(square instanceof Shape); // true
Copy the code

Alternatively, we can set this to be the prototype of the instance prototype so that we can use both Instanceof and GET. In the example below, we have a prototype chain Object -> Outer Shape -> Inner Shape prototype -> Inner Shape.

function Shape() {
  // Set of private variables
  const this$= {};class Shape {
    constructor(width, height) {
      this$.width = width;
      this$.height = height;
    }

    get area() {
      return this$.width * this$.height; }}const instance = newShape(... arguments);Object.setPrototypeOf(Object.getPrototypeOf(instance), this);
  return instance;
}

const square = new Shape(10.10);
console.log(square.area);             / / 100
console.log(square.width);            // undefined
console.log(square instanceof Shape); // true
Copy the code

Proxy

Proxy is a nice new feature in JavaScript that allows you to effectively wrap an object in an object called Proxy and intercept all interactions with that object. We will use the Proxy and follow the naming convention above to create private variables, but we can make these private variables inaccessible outside the class.

Proxy can intercept many different types of interactions, but let’s focus on GET and SET. Proxy allows us to intercept read and write operations on a property, respectively. When you create a Proxy, you provide two parameters, the first being the instance that you intend to wrap, and the second a “handler” object that you define that you want to intercept the different methods.

Our processor will look something like this:

const handler = {
  get: function(target, key) {
    if (key[0= = ='_') {
      throw new Error('Attempt to access private property');
    }
    return target[key];
  },
  set: function(target, key, value) {
    if (key[0= = ='_') {
      throw new Error('Attempt to access private property'); } target[key] = value; }};Copy the code

In each case, we check if the name of the property being accessed begins with an underscore, and if so we throw an error to prevent access to it.

class Shape {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }
  get area() {
    return this._width * this._height; }}const handler = {
  get: function(target, key) {
    if (key[0= = ='_') {
      throw new Error('Attempt to access private property');
    }
    return target[key];
  },
  set: function(target, key, value) {
    if (key[0= = ='_') {
      throw new Error('Attempt to access private property'); } target[key] = value; }}const square = new Proxy(new Shape(10.10), handler);
console.log(square.area);             / / 100
console.log(square instanceof Shape); // true
square._width = 200;                  // Error: Attempting to access private properties
Copy the code

As you can see in this example, we retain the ability to use Instanceof and there are no unexpected consequences.

Unfortunately, we have problems when we try to execute json.stringify because it tries to format private attributes. To solve this problem, we need to rewrite the toJSON function to return only “public” attributes. We can handle the specific case of toJSON by updating our GET handler:

Note: This will override any custom toJSON functions.

get: function(target, key) {
  if (key[0= = ='_') {
    throw new Error('Attempt to access private property');
  } else if (key === 'toJSON') {
    const obj = {};
    for (const key in target) {
      if (key[0]! = ='_') {           // Only public attributes are copiedobj[key] = target[key]; }}return (a)= > obj;
  }
  return target[key];
}
Copy the code

We have now closed our private properties and the expected functionality remains, with the only caveat that our private properties can still be traversed. For (const key in square) lists _width and _height. Thankfully, a processor is also available here! We can also intercept calls to getOwnPropertyDescriptor and manipulate the output of our private property:

getOwnPropertyDescriptor(target, key) {
  const desc = Object.getOwnPropertyDescriptor(target, key);
  if (key[0= = ='_') {
    desc.enumerable = false;
  }
  return desc;
}
Copy the code

Now we put all the features together:

class Shape {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }
  get area() {
    return this._width * this._height; }}const handler = {
  get: function(target, key) {
    if (key[0= = ='_') {
      throw new Error('Attempt to access private property');
    } else if (key === 'toJSON') {
      const obj = {};
      for (const key in target) {
        if (key[0]! = ='_') { obj[key] = target[key]; }}return (a)= > obj;
    }
    return target[key];
  },
  set: function(target, key, value) {
    if (key[0= = ='_') {
      throw new Error('Attempt to access private property');
    }
    target[key] = value;
  },
  getOwnPropertyDescriptor(target, key) {
    const desc = Object.getOwnPropertyDescriptor(target, key);
    if (key[0= = ='_') {
      desc.enumerable = false;
    }
    returndesc; }}const square = new Proxy(new Shape(10.10), handler);
console.log(square.area);             / / 100
console.log(square instanceof Shape); // true
console.log(JSON.stringify(square));  / / "{}"
for (const key in square) {           // No output
  console.log(key);
}
square._width = 200;                  // Error: Attempting to access private properties
Copy the code

Proxy is my favorite method for creating private properties in JavaScript at this point. This class is built in a way that is familiar to old-school JS developers, so it can be compatible with old, existing code by wrapping them in the same Proxy processor.

Bonus: Processing in TypeScript

TypeScript is a superset of JavaScript that compiles to native JavaScript for production use. Allowing you to specify private, public, or protected properties is one of TypeScript’s features.

class Shape {
  private width;
  private height;

  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  get area() {
    return this.width * this.height; }}const square = new Shape(10.10)
console.log(square.area); / / 100
Copy the code

The important thing to note about TypeScript is that it only learns about these types at compile time, and that private and public modifiers have an effect at compile time. If you try to access square.width, it works. TypeScript just sends you an error at compile time, but doesn’t stop compiling.

// Compile-time error: Property 'width' is private and can only be accessed in the 'Shape' class.
console.log(square.width); / / 10
Copy the code

TypeScript doesn’t smartly do anything to try to prevent code from accessing private properties at runtime. I’m only putting it here, just to make you realize that it doesn’t solve the problem directly. Take a look at the JavaScript code created by TypeScript above for yourself.

In the future

I’ve shown you what you can do now, but what about the future? In fact, the future looks interesting. There is currently a proposal to introduce private fields into JavaScript classes, which use the # symbol to indicate that they are private. It is used in a very similar way to the naming convention technique, but provides practical restrictions on variable access.

class Shape { #height; #width; constructor(width, height) { this.#width = width; this.#height = height; } get area() { return this.#width * this.#height; } } const square = new Shape(10, 10); console.log(square.area); // 100 console.log(square instanceof Shape); // true console.log(square.#width); // Error: Private attributes can only be accessed in a classCopy the code

If you’re interested, you can read the full proposal below to get closer to the truth. What I find interesting is that private attributes need to be predefined and cannot be created or destroyed temporarily. To me, this feels like a very foreign concept in JavaScript, so it will be interesting to see how this proposal continues to evolve. Currently, the proposal focuses on private class attributes rather than private function or object level private members, which may come later.

NPM package – Privatise

As I write this article, I also released a NPM package to help create privatise properties. I used the Proxy approach described above and added additional handlers to allow the class itself to be passed in instead of an instance. All codes can be found on GitHub and any PR or Issue is welcome.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.