Private data in objects is a trickier proposition in JavaScript than in most languages. Only the most recent spec (ES12) specifically addresses private data, so the way to hide it has until recently been to put it in a closure.
There are several ways to do this.
Object factories
One way to do this is through an object factory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function makeMan(fName, lName) { let honorific = 'Mr.'; return { fullName() { return `${honorific} ${fName} ${lName}`; } }; } let bob = makeMan('Bob', 'Rodes'); console.log(bob.fullName()); // Mr. Bob Rodes console.log(bob.honorific, bob.fName, bob.lName); // undefined undefined undefined |
In this implementation, fName
, lName
and honorific
are part of the returned object’s closure. As such, they are available to the returned object’s methods, but not exposed as properties of the object.
Constructor function
A constructor function can hold private variables in similar fashion:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function Man(fName, lName) { let honorific = 'Mr.'; this.fullName = function() { return `${honorific} ${fName} ${lName}`; } } let me = new Man('Bob', 'Rodes'); console.log(me.fullName()); // Mr. Bob Rodes console.log(me.honorific, me.fName, me.lName); // undefined undefined undefined |
Duplication problem with object factories and constructor functions
Both object factories and constructor functions require each object instance to carry its own copy of all the object’s methods. This isn’t very scalable.
The usual way to deal with the duplication problem is to put methods in the prototype. But when there is private data, doing this loses access to the data:
1 2 3 4 5 6 7 8 9 10 11 12 |
function Man(fName, lName) { let honorific = 'Mr.'; } Man.prototype.fullName = function() { return `${honorific} ${fName} ${lName}`; } let me = new Man('Bob', 'Rodes'); console.log(me.fullName()); // ReferenceError: honorific is not defined |
Now that the fullName
method is assigned to the prototype, it no longer has access to the Man
constructor’s closure. So, it can’t see the private variables.
This is a non-trivial problem in JavaScript. For efficiency reasons, it is preferable to define methods in an object’s prototype, so that every instance shares a single copy. But it is difficult to make private instance data available to the prototype’s methods.
A solution with IIFE and the WeakMap
object
Part of a solution is to use an IIFE to wrap private data, a constructor function and prototype methods in a single closure. This is only a partial solution because, while it does manage to expose the private data to the prototype method, it exposes only a single copy of the data to all instances. Therefore, a change by one instance becomes a change to all the others:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
let Man = (function() { let honorific = 'Mr.'; let fName; let lName; function Man(fN, lN) { fName = fN; lName = lN; } Man.prototype.fullName = function() { return `${honorific} ${fName} ${lName}`; }; return Man; })(); let bob = new Man('Bob', 'Rodes'); let greg = new Man('Grzegorz', 'Przybysz'); console.log(bob.fullName()); // Mr. Grzegorz Przybysz (Should be Mr. Bob Rodes) |
A common way to resolve this is to create a single private WeakMap
object, and store all the private data in it. A WeakMap
object is a set of key/value pairs. While all object instances share the same WeakMap
object, the WeakMap
object itself is capable of keeping separate objects’ private data separate.
In the WeakMap
, each key is an object’s this
, and each value is an object containing the object’s private data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
let Man = (function() { let private = new WeakMap(); function Man(fName, lName) { let privateData = { honorific: 'Mr.', fName: fName, lName: lName, } private.set(this, privateData); } Man.prototype.fullName = function() { const prData = private.get(this); return `${prData.honorific} ${prData.fName} ${prData.lName}`; }; return Man; })(); let bob = new Man('Bob', 'Rodes'); let greg = new Man('Grzegorz', 'Przybysz'); console.log(bob.fullName()); // Mr. Bob Rodes console.log(greg.fullName()); // Mr. Grzegorz Przybsz console.log(bob.fullName === greg.fullName); // true |
While one may also use the Map
object in this way, the WeakMap
object is a better solution, because its key/value pairs are able to be garbage collected independently of one another. This means that if a WeakMap
object is used to store private data, the lifetime of the data is tied to the lifetime of the object. This is not the case with a Map
object, which must go out of scope in entirety before it becomes eligible for garbage collection. This means that if a Map
object refers to multiple objects, the objects’ private properties will persist so long as the Map
object contains any live references.
The “modern” way: ES12 and the PrivateIdentifier
production
With the introduction of the PrivateIdentifier
production (as defined by ECMA, a production is an “entity in a context-free grammar”) in ES12, JavaScript has considerably simplified implementation of private properties. Syntactically, the PrivateIdentifier
production is implemented as a property preceded by a #
character. This syntax is only valid inside a class
definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Man { #honorific; #fName; #lName; constructor(fName, lName) { this.#honorific = 'Mr.'; this.#fName = fName; this.#lName = lName; } fullName() { return `${this.#honorific} ${this.#fName} ${this.#lName}`; }; } let bob = new Man('Bob', 'Rodes'); let greg = new Man('Grzegorz', 'Przybysz'); console.log(bob.fullName()); // Mr. Bob Rodes console.log(greg.fullName()); // Mr. Grzegorz Przybsz console.log(bob.fullName === greg.fullName); // true console.log(bob.#fName); // SyntaxError: Private field '#fName' must be declared in an enclosing class |
Although all popular browsers support this, it is still quite new, and organizations that are conservative about updating their browser versions may not support it.
For more information on browser version compatibility, scroll to the bottom of the doc for private class features.