references:
- https://stackoverflow.com/questions/22156326/private-properties-in-javascript-es6-classes
- https://www.javascriptjanuary.com/blog/es6-classes
- https://ttmm.io/tech/private-variables-in-javascript/
- https://esdiscuss.org/topic/es6-problem-with-private-name-objects-syntax
ECMAScript 2015 (aka ES6) introduced classes to JavaScript.
Older versions of the language would allow you to define an object type directly using function constructors, but didn’t truly support object-oriented inheritance the way developers are used to.
Part of this is due to the fact that JavaScript is a prototypal language, not an object-oriented language. Or at least older versions weren’t.
ES6 gave us classes with proper constructors, methods, properties, and inheritance.
You could define a real class in ES6 and reuse it the same way you would a type in any other language:
1 2 3 4 5 6 |
class Rectangle { constructor(height, width) { this.height = height; this.width = width; } } |
The biggest unfortunate element of this class definition, though, is that there are no privacy modifiers.
The constructor, methods, and properties of a JS class are all public by default.
There’s no nature of a protected method or a private property. At all.
Module Privacy
1 2 3 4 5 6 7 8 9 10 11 |
let name = null; class Client { constructor(newName) { name = newName } toString() { return name; } } |
The name variable inside this module is completely inaccessible to any code outside the module, yet it allows our class to keep track of this name property directly. This is exactly what we want, right?
Nope.
Since it’s outside the class definition, this variable is treated as a static property. Yes, it’s private to our module, but there’s only one copy available. If we make multiple instances of our Client above, we’ll overwrite name each time; the last instantiation will win and define the name used by all of the instances.
Hack
We create a static literal object called container. When an instance of Client is created, we give the instance an id, then use the instance id
to create a key in the container object. Its corresponding value would be an empty object where we store the name variable.
Thus, each instance at least can keep track of its own properties and usage.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let container = {} class Client { constructor(newName) { this._id = Math.random().toString(36).substr(2, 9) container[this._id] = {} container[this._id].name = newName } toString() { return container[this._id].name } } |
Not attaching the new properties to the object, but keeping them inside a class constructor
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
class CircularList { constructor(listName) { var _name = listName // _name is PRIVATE // set _name through setName this.setName = function(listName) { _name = listName;} // get _name through getName this.getName = function() { return _name; } // private member variables var _head = null; this.setHead = function(newHead) {_head = newHead;} this.getHead = function() {return _head;} var _tail = null; this.setTail = function(newTail) {_tail = newTail;} this.getTail = function() {return _tail;} var _now = null; this.setNow = function(newNow) {_now=newNow;} this.getNow = function() {return _now;} console.log("Finished constructing CircularList") } // constructor isEmpty() { return (this.getHead() == null && this.getTail() == null); } print() { if (this.getHead() === null) { console.log("-- LIST IS EMPTY --"); return; } var traversal = this.getHead(); do { traversal.display(); traversal = traversal.next; } while (traversal !== this.getHead() && traversal != null); } ... ... ... } |
Hiding with Weak Maps
https://javascript.info/map-set-weakmap-weakset
https://stackoverflow.com/questions/22156326/private-properties-in-javascript-es6-classes
WeakMaps associate data with Objects (here, instances) in such a way that it can only be accessed using that WeakMap. So, we use the scoped variables method to create a private WeakMap, then use that WeakMap to retrieve private data associated with this. This is faster than the scoped variables method because all your instances can share a single WeakMap, so you don’t need to recreate methods just to make them access their own WeakMaps.
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 31 32 33 34 35 36 37 38 39 40 41 42 |
// Any variables declared inside a block, is scoped to the nearest function. // create closure with IIFE, thus all variables declared are private to the // nearest function //The first difference from Map is that its keys must be objects, not primitive values: let Person = (function () { console.log(" --- scope of IIFE ---") // private to this scope let privateProps = new WeakMap(); let jKey = {name:"John"} class Person { constructor(name) { console.log("constructing Person....") this.name = name; // this is public // sets instance 'this' to property value privateProps.set(this, {age: 20, SSN:"NA"}); // this is private privateProps.set(jKey, "testing 1 2 3"); } greet() { console.log("calling greet function") console.log(privateProps.has(jKey)) console.log(privateProps.has(this)) console.log(privateProps) // Here we can access both name and age console.log(`name: ${this.name}, age: ${privateProps.get(this).age}`); console.log(`SSN - ${privateProps.get(this).SSN}`); console.log(privateProps.get(jKey)); } } console.log(" returning Person class"); return Person; // returns class Person to outside variable Person })(); let rickyPerson = new Person("ricky"); rickyPerson.greet(); |
WeakMap, another way
https://chrisrng.svbtle.com/using-weakmap-for-private-properties
WeakMap Technique
Traditionally, the way to have private properties in Javascript is to either prefix your variables or to encapsulate in a closure. Both of these methods do work, however they are either not restrictive enough (prefixes) or too restrictive (closures).
A WeakMap is similar to a HashMap but it allows objects as keys as well as not having a strong reference to its values (that’s why it’s called weak).
In the example below, we pass in an empty object into the WeakMap to hold all of the private properties of the class Wizard. To store the private properties, we also pass in the reference to the unique this object of the instance of the class Wizard as a key to the WeakMap.
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 31 32 33 34 35 36 37 |
const Wizard = (function() { const _private = new WeakMap(); const internal = (key) => { // Initialize if not created if (!_private.has(key)) { _private.set(key, {}); } // Return private properties object return _private.get(key); }; class Wizard { constructor(name, house) { internal(this).name = name; internal(this).house = house; } getName() { return internal(this).name; } setName(name) { internal(this).name = name; } getHouse() { return internal(this).house; } setHouse(house) { internal(this).house = house; } } return Wizard; }()); |
When we actually run this code, you can see on the second line that when we try to access the property _private the class returned does not have reference to it because of the IIFE. However internal class methods can still change the values under _private. So you can only change the properties within the Class by using its accessor methods such as get and set. In doing so, keeping the namespace hidden from all functions except members of the class effectively implements private properties.
1 2 3 4 5 6 |
const harrypotter = new Wizard('Harry Potter', 'Gryffindor'); console.log(harrypotter); // Wizard {} console.log(harrypotter.getName()); // "Harry Potter" harrypotter.setName('Arry Pottr'); console.log(harrypotter.getName()); // "Arry Pottr" |
Break Down
ref – https://stackoverflow.com/questions/22156326/private-properties-in-javascript-es6-classes
The only truly private data in JavaScript is still scoped variables. You can’t have private properties in the sense of properties accessed internally the same way as public properties, but you can use scoped variables to store private data.
Scoped variables
The approach here is to use the scope of the constructor function, which is private, to store private data.
For methods to have access to this private data they must be created within the constructor as well, meaning you’re recreating them with every instance.
This is a performance and memory penalty, but some believe the penalty is acceptable. The penalty can be avoided for methods that do not need access to private data by adding them to the prototype as usual.
Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function Person(name) { let age = 20; // this is private this.name = name; // this is public this.greet = function () { // here we can access both name and age console.log(`name: ${this.name}, age: ${age}`); }; } let joe = new Person('Joe'); joe.greet(); // here we can access name but not age |
Scoped WeakMap
A WeakMap can be used to avoid the previous approach’s performance and memory penalty.
WeakMaps associate data with Objects (here, instances) in such a way that it can only be accessed using that WeakMap. So, we use the scoped variables method to create a private WeakMap, then use that WeakMap to retrieve private data associated with this. This is faster than the scoped variables method because all your instances can share a single WeakMap, so you don’t need to recreate methods just to make them access their own WeakMaps.
Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
let Person = (function () { let privateProps = new WeakMap(); class Person { constructor(name) { this.name = name; // this is public privateProps.set(this, {age: 20}); // this is private } greet() { // Here we can access both name and age console.log(`name: ${this.name}, age: ${privateProps.get(this).age}`); } } return Person; })(); let joe = new Person('Joe'); joe.greet(); // here we can access joe's name but not age |
This example uses an Object to use one WeakMap for multiple private properties; you could also use multiple WeakMaps and use them like age.set(this, 20), or write a small wrapper and use it another way, like privateProps.set(this, ‘age’, 0).
The privacy of this approach could theoretically be breached by tampering with the global WeakMap object
Say you’re coding up a JS app and you import another file written by Fred for usage.
Then in your current file, you create your class and then use WeakMap to store your private variables.
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var b64Coder = require("./base64Coder"); const privateProp1 = new WeakMap(); const privateProp2 = new WeakMap(); class SomeClass { constructor() { console.log('---- creating SomeClass instance ----'); privateProp1.set(this, "I am Private1"); privateProp2.set(this, "I am Private2"); this.publicVar = "I am public"; this.publicMethod = () => { console.log(privateProp1.get(this), privateProp2.get(this)) }; } printPrivate() { console.log(privateProp1.get(this)); } } |
output:
I am Private1 I am Private2
However, say Fred is a bit naughty and decide to overwrite the global object Weakmap like so:
base64Coder.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// boom! tampers with global WeakMap' prototype object. yikes! var oldSet = WeakMap.prototype.set; WeakMap.prototype.set = function(key, value) { // Fred's evil implementation // Tampers with 'this', 'key', and 'value' return oldSet.call(this, key, value); }; function base64Coder() { ... ... } |
So Fred decides to repoint the set reference in Weakmap’s prototype to his own custom function. But before doing so, in order not to lose the original function, he’ll retain it with a reference called oldSet.
He then has access to your key/values and can potentially change them, or even store them somewhere else. When he’s done, he’ll just return the original function, pointed to by referenceoldSet.
That said, all JavaScript can be broken by mangled globals. Our code is already built on the assumption that this isn’t happening.
Solution to fix this
In order to remedy this, you first declare references to the original set/get functions on the Weakmap prototype.
This ensures you have access to them. Now, when the tamperer decides to inject their own function, what they’re doing is re-pointing Weakmap.prototype.set to their own evil function as shown above. Thus, you don’t have to worry about that because you have already set your own reference to the correct original functions like so:
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 |
const {set: WMSet, get: WMGet} = WeakMap.prototype; var b64Coder = require("./base64Coder"); var CryptoJS = require('crypto-js'); var SECRET_KEY = 'HOHOHO'; // other code // now, when we use it, we won't be affected const privateProp1 = new WeakMap(); const privateProp2 = new WeakMap(); class SomeClass { constructor() { console.log('constructing SomeClass instance...'); // using the original set function, not affected by any tampering WMSet.call(privateProp1, this, "I am Private1"); WMSet.call(privateProp2, this, "I am Private2"); this.publicVar = "I am public"; this.publicMethod = () => { console.log(WMGet.call(privateProp1, this), WMGet.call(privateProp2, this)) }; } printPrivate() { console.log(WMGet.call(privateProp1, this)); } } |
The way how it used is like so. The first object is the object you’re using to execute the function call. The next parameters get matched up with the function parmaeters.
1 2 3 4 5 6 7 8 9 10 |
var person = { fullName: function(city, country) { return this.firstName + " " + this.lastName + "," + city + "," + country; } } var person1 = { firstName:"John", lastName: "Doe" } person.fullName.call(person1, "Oslo", "Norway"); |
Just make sure you do it at the top of your file before you import/require the questioning code components.
Half Answer – Scoped Symbols
A Symbol is a type of primitive value that can serve as a property name. You can use the scoped variable method to create a private Symbol, then store private data at this[mySymbol].
The privacy of this method can be breached using Object.getOwnPropertySymbols, but is somewhat awkward to do.
Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
let Person = (function () { let ageKey = Symbol(); class Person { constructor(name) { this.name = name; // this is public this[ageKey] = 20; // this is intended to be private } greet() { // Here we can access both name and age console.log(`name: ${this.name}, age: ${this[ageKey]}`); } } return Person; })(); let joe = new Person('Joe'); joe.greet(); // Here we can access joe's name and, with a little effort, age. ageKey is // not in scope, but we can obtain it by listing all Symbol properties on // joe with `Object.getOwnPropertySymbols(joe)`. |
For example, this is how you would expose it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
let Weather = (function () { var nameKey = Symbol('nameKey'); class Weather { constructor() { this[nameKey] = 'SECRET! Do not expose!'; ... ... } ... ... } let w = new Weather(WEATHER_API, WEATHER_LOCATION_STRING, OPEN_WEATHER_MAP_KEY); w.fetchWeatherData(); w.test(); let result = Object.getOwnPropertySymbols(w); console.log('result', result); // result (1) [Symbol(nameKey)] let res = w[result[0]]; console.log(res); // exposed! |
Half-Answer: Underscores
The old default, just use a public property with an underscore prefix. Though not a private property in any way, this convention is prevalent enough that it does a good job communicating that readers should treat the property as private, which often gets the job done. In exchange for this lapse, we get an approach that’s easier to read, easier to type, and faster.
Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Person { constructor(name) { this.name = name; // this is public this._age = 20; // this is intended to be private } greet() { // Here we can access both name and age console.log(`name: ${this.name}, age: ${this._age}`); } } let joe = new Person('Joe'); joe.greet(); // Here we can access both joe's name and age. But we know we aren't // supposed to access his age, which just might stop us. Conclusion As of ES2017, there's still no perfect way to do private properties. Various approaches have pros and cons. Scoped variables are truly private; scoped WeakMaps are very private and more practical than scoped variables; scoped Symbols are reasonably private and reasonably practical; underscores are often private enough and very practical. |