DEV Community

allen-woods
allen-woods

Posted on

Defining Private Members in JavaScript Classes: How and Why

Context

One of the more important concepts in Object Oriented Programming is the use of private properties and methods, collectively referred to as private members from here on.

The basic idea is that any data that is critical data to an application should be made private to prevent catastrophic faults or exploits that would otherwise be possible if those private members were accessible.

While it is possible to achieve truly private members using technologies such as webpack or ES6 modules, I will be discussing how you can achieve a very similar result using closures, Symbol(), and WeakMap().

For more details on this subject that go beyond the scope of this article, please give the inspiration for this article, by Axel Rauschmayer a read.

Naming Convention

When researching how to accomplish private members on classes in vanilla JavaScript, I found several articles and Stackoverflow threads suggesting the use of a common naming convention that places underscores at the beginning of variable names to indicate a "private" status.

class NamingConventionClass {
  constructor() {
    this._hidden = "Can you see me?";
  }
};

The problem with this approach is that it is a way to indicate to other programmers whether a member is intended to be private or not. In short, something that is "_hidden" through naming convention is still visible.

let peekaboo = new NamingConventionClass();
console.log(peekaboo._hidden + '...Yes I can.');

// outputs 'Can you see me? ...Yes I can.'

This is a problem if the intention is to keep certain members on our class objects inaccessible from the outside. Anybody using inspect in their browser could manipulate variables with undesirable results that could have real consequences beyond DOM manipulation.

We need some sort of private container to place our private members into without losing track of either where they are or what instance they belong to.

Enter the WeakMap

The best solution provided by vanilla JavaScript that I have found (as of this writing) is the WeakMap class. A WeakMap is simply a collection of keys and values. Each key must be a reference to an object and each corresponding value can be any arbitrary data. Because each key, value pair is added at the same time, their indexes always correspond.

let myWeakMap = new WeakMap();

The main advantage of using WeakMaps as opposed to other options provided by the map API is that their references are held "weakly", allowing for garbage collection. This means that if the data referenced in the WeakMap is deleted, such as the destruction of a class instance, the allocation of the WeakMap will be removed from memory along with it.

One way to use WeakMaps to obfuscate private members from a class is to do something similar to the example provided in Axel's article, as follows:

let _variableOne = new WeakMap();
let _variableTwo = new WeakMap();

class WeakMapPrivateMembers {
  constructor(valueOne, valueTwo) {
    _variableOne.set(this, valueOne);
    _variableTwo.set(this, valueTwo);
  }
  combineValuesInString() {
    let v1 = _variableOne.get(this);
    let v2 = _variableTwo.get(this);
    return `${v1}, ${v2}`;
  }
};

const hideTwoVariables = new WeakMapPrivateMembers('foo', 'bar');
console.log(hideTwoVariables.combineValuesInString());

This approach has at least three major problems listed below.

  • WeakMaps are accessible in global namespace.
  • WeakMaps are inefficiently defined for each variable.
  • WeakMaps can still be accessed and overwritten.

We're making progress, but now we have a problem based on the scope that our data is being stored in. We need to redefine how our class is being instantiated in such a way that our WeakMap data can remain inaccessible.

Class as Closure

It turns out that you can wrap an entire class definition inside of an Immediately Invoked Function Expression, or IIFE (pronounced "iffy"). The basic syntax of how this would look is as follows:

const MyClosureClass = ((param1, param2, param3) => {

  // Define a variable inside the scope of the closure to keep it hidden.
  // This WeakMap will act as a repository for all instances of this class.

  const _wm = new WeakMap();

  return class MyClosureClass {
    constructor(param1, param2, param3) {
      // add this class instance's private members

      _wm.set(this, {
        property1: param1,
        property2: param2,
        property3: param3
      });
    }
    get privateMembers() {
      // return the object stored at object key of "this"

      return _wm.get(this);
    }
  };
})();

let myPrivacy = new MyClosureClass("something important");

console.log(myPrivacy.privateMembers);
// The above line will return 'something important'

console.log(myPrivacy.params);
// The above line will return undefined

console.log(_wm);
// The above line will return undefined as well

What this means is that our class can now be instantiated with incoming data that can only be obtained through sanctioned getter methods. Also, we can nest an object containing a more complex structure of private members we want to associate with the class instance.

We could take this a step further and apply transformations to the data before returning it, or simply use the privately stored value(s) to perform conditionals that dictate the return of safer values.

// (code abridged)

    // rewrite the getter method as a confirmation method
    hasPrivateMembers() {
      return !!_wm.get(this);
    }

// (code continued)

It is important to note that you should be writing a helper method in the scope of your closure to abstract the need to type _wm.get(this).params[privateMemberName] every time you want to access the private member privateMemberName, as shown below.

// Inside the scope of our closure...

const _self = function(objRef) {
  _wm.get(objRef);
}

Embedding Symbols to Introduce Entropy

If you really, really, really need something to be private, other options are more suitable for this -- securing data within your database with no client side visibility being chief among them.

However, if you want to take the privacy of client side data to a paranoid level, you can also nest your private members deeper by using a Symbol to point to them.

// (code abridged)
    constructor(param1, param2, param3) {
      _wm.set(this, {
        [Symbol('root')]: {
          property1: param1,
          property2: param2,
          property3: param3
        }
      });
    }
// (code continued)

The primary drawback to applying this emphatic level of privacy is that you need a rather convoluted helper method to access these private members. However, if you want to pursue this route to explore how it operates, the helper method would look something like this:

// Inside the scope of our closure...

const _self = function(objRef) {
  let obj = _wm.get(objRef)[
    Object.getOwnPropertySymbols( _wm.get(objRef) )[0]
  ];

  return obj;
}

Conclusion

If you don't want to rely on an entire framework to do the work for you, using a WeakMap and an IIFE wrapper to convert your classes into closures is a terrific solution to private members on classes as of this writing.

I hope that this article can help guide you in improving your design considerations when building OOJS applications that require both public and private members.

Here in its final form is a reference of our example class utilizing everything we have just discussed.

const MyClosureClass = ((param1, param2, param3) => {

  const _wm = new WeakMap();

  const _self = function(objRef) {
    let obj = _wm.get(objRef)[
      Object.getOwnPropertySymbols( _wm.get(this) )[0]
    ];
    return obj;
  };

  return class MyClosureClass {
    constructor(param1, param2, param3) {

      _wm.set(this, {
        [Symbol('root')]: {
          property1: param1,
          property2: param2,
          property3: param3
        }
      });
    }
    get privateMembers() {
      return _self(this);
    }

    getSpecificMember(memberName) {
      return _self(this)[memberName];
    }

    hasPrivateMembers() {
      return !!_self(this);
    }
  };
})();

Top comments (0)