JavaScript’s Journey To Privacy

TOAST UI
9 min readApr 27, 2020

--

Among the specs for the ECMAScript class fields is a Private field, or a Private property. Since class fields are experimental features, currently at stage 3 (candidate) of development, they will soon pass the stage 4 (finished) to become standardized specs. When the experimental feature first came out, I had mixed first impressions of excitement for possible private properties and disappointment with its syntax. And since then, time passed and I forgot about it. That is until I heard that the set of experimental features will be officially supported by TypeScript 3.8, and I decided to take the time to properly learn about the private properties. Although, I have been using the public classes through the babel-plugin-proposal-class-properties since long time ago.

It Sucks Without Privacy

Because JavaScript cannot officially make private properties, JavaScript developers have built other means. JavaScript cannot make properties fundamentally private like other class-based languages, so JavaScript sort-of-kind-of cheated by promising conventions to emulate privacy.

One of the most commonly used convention is to prefix the name of a property with an underscore (_). Such method is also used with Python.

function SomeConstructor() {
this._privateProp = 'don\'t touch this';
this.publicProp = 'you can touch this';
}

I used the function constructor instead of the class keyword to embolden the good ol’ days vibe.

This way of privatizing is merely a convention, and properties privatized are officially classified as public and can be accessed from the outside. However, it is still a good practice to adhere to the convention saying that the underscored properties shall not be accessed from the outside as it helps with readability of the code. Just like how we mindlessly use i for iterating for-loops, if everyone follows the convention, it is quite effective. Douglas Crockford has also said on his blog that such method should be avoided as this can make programmers mistake not private fields to be private, if the everyone promises to follow the same convention reliably, it is the next best thing.

When JSDoc became mainstream, there was an increase in the number of editors that started to offer JSDoc tags for informative and convenience purposes other than its original documentation automation purposes. JSDoc has now evolved beyond the documentation generator and into an extended syntax that enhanced the expressiveness of a language through comments. With JSDoc, the @private tag can be used to represent privacy. How great is it that you can actually make a private field without the ambiguous underscore and that it generates the documentation automatically? Now, there are more and more people refusing to use the underscore convention in the realm of JavaScript, and I agreed to boycott the use of underscore convention for this exact reason.

For other ways of creating inaccessible private fields, a closure is one such example. This method has also been suggested by Crockford as well. While JavaScript can always surprise you out of nowhere with new ingenuity, the closure method is most commonly used. To go even one step further, it is actually a great use of a closure. (Reference: Closure, Encapsulation, and Privatization)

function SomeConstructor() {
const privateProp = 'dont touch this';
this.publicProp = 'you can touch this';
this.doSomethingWithPrivateProp = () => { ... }
}

Since this appears to be different from using the this to access the data, it can make the code difficult to read, but it was effective in separating the accessible data from the private data. The level of separation is a dimensional separation between the instance contexts. This method of access is not only useful for hiding the data but is also useful for hiding the methods. Such hiding techniques have been used to implement module patterns.

function SomeModule() {
const privateProp = 'dont touch this';
const publicProp = 'you can touch this';
_doSomethingWithPrivateProp = () => { ... } const publicMethod = () => {
_doSomethingWithPrivateProp();
// ...
}
return {
publicProp,
publicMethod
}
}

Module patterns are effective in certain areas like the areas where the use of high-level interface or the ES6 modules are not allowed, but with the rise of ES6 modules (ESM), it has lost its base in the front-end development. I don’t even remember the last time when I used the module pattern in my codes and I think it’s been at least five years. Such issues are why ESMs have been developed in the first place. When you take a look at the webpack transpiled ESM codes, it resembles the original module patterns. While the ESM tried its best to satisfy the need for privacy, not much has been actually done in that we still needed to create private data for each constructor’s instance context. We were going the wrong way.

If we were to use the Symbol, we can get even closer to the pseudo-private property that is more ECMAScriptlike. This method, however, is more than a pseudo-private and is an optimal way of utilizing the ES6 resources. I daydreamed about it and its awesomeness earlier, but I never had the chance to use it in the workplace. Well, that, and now I guess they officially support private or whatever...

const privateMethodName = Symbol();
const privatePropName = Symbol();
class SomeClass {
[privatePropName] = 'dont touch this';;
publicProp = 'you can touch this';
[privateMethodName]() {
console.log('private method');
}
publicMethod() {
this[privateMethodName](this[privatePropName]);
}
}

Within the module scope, you can access the corresponding fields and methods with the use of symbols, but in the outside, where the symbols have not been exported, there is no way to access them. You simply do not know the correct names for the appropriate access. This is an example of separating the names of properties in the dimensional level.

Some Good Hash (#)

Finally, we have aan official way of making private properties within our classes in JavaScript instead of sneaky conventions. The TC39 Spec Documentation can be summarized as such.

  • Specs in the Stage-3 will most likely make it to the standarized specs unless there is a significant reason for dismissal. However, it can still be changed or improved.
  • The private keyword will not be used. Instead, the hash # prefix will be used. It is not a keyword; it is a prefix. By prefixing the property name with a hash, you can make the property private.
  • It is part of the Class Field Declarations specs. The difference between the private and the public fields is that the private field can only be made through field declaration. In other words, the private fields cannot be added to objects dynamically.
  • Methods are limited. It cannot be used as methods declarations. You can make private methods through proper function representations. This is only currently speaking. Specs may be updated. (Class fields and private methods: Stage 3 update)
  • Computed Property Names are not allowed. Only #foo itself is recognized as private, and #[fooname] will throw a syntax error.
  • All private fields will have its own unique scope within the located class. Therefore, it comes with interesting characteristics that we will cover later.

Currently, it does not have the support other class based languages have. While there are some restrictions, it is still stage-3, and can always be changed and updated. This makes me wonder why private methods were not included in the discussion in the first place.

Now, let’s actually get started on using them. (In order to read the error messages, we will use the TypeScript compilers. However, the examples consist only of ECMAScript syntax.)

class Human {
#age = 10;
}
const person = new Human();

We have used the hash # prefix to add a #age property to the Human class.

Let’s test if this is actually private, head on.

console.log(person.#age); // Error TS18013: Property '#age' is not accessible outside class 'Human' because it has a private identifier.

Obviously, we get an error message saying that it cannot be accessed from the outside.

Also, like I mentioned above, hash # is a prefix not a keyword.

class Human {
#age = 10;
getAge() {
return this.age; // Error TS2551: Property 'age' does not exist on type 'Human'. Did you mean '#age'?
}
}

The age cannot be accessed without the #. This is because we missed a portion of the identifier's name, so we were trying to access a property that does not exist.

Now, let’s properly define and use the private property within the class.

class Human {
#age = 10;
getAge() {
return this.#age;
}
}
const person = new Human();console.log(person.getAge()); // 10

We have allowed the #age to be accessible from the outside through the getAge() getter.

While it may be obvious, private properties cannot be accessed anywhere else than in the class where it belongs. Even inherited classes do not have the access to it. I mentioned this specifically for those who still refuse to believe that JavaScript is functioning logically.

class Human {
#age = 10;
getAge() {
return this.#age;
}
}
class Person extends Human {
getFakeAge() {
return this.#age - 3; // Property '#age' is not accessible outside class 'Human' because it has a private identifier.
}
}

The Person has inherited from Human, so Person cannot access the Human's private #age property.

However, there is a little caveat. This is not a caveat of private properties but of JavaScript. This happens because of the All private fields will have its own unique scope within the located class aspect of the previous summary.

class Human {
age = 10;
getAge() {
return this.age;
}
}
class Person extends Human {
age = 20;
getFakeAge() {
return this.age;
}
}
const p = new Person();
console.log(p.getAge()); // 20
console.log(p.getFakeAge()); // 20

The above example does not have a single bit of privacy. The Person object that inherited from Human object has repeatedly declared the age and defined a different getFakeAge() getter. If it were a public property, there would be a single age properties within this context of value 20. This means that whether you run getAge() from the Human or the getFakeAge() from the Person, you should get 20 identically. Because in the context where this points to, there is only one age in the instance context.

Now let’s change the age to private #age.

class Human {
#age = 10;
getAge() {
return this.#age;
}
}
class Person extends Human {
#age = 20;
getFakeAge() {
return this.#age;
}
}
const p = new Person();
console.log(p.getAge()); // 10
console.log(p.getFakeAge()); // 20

getAge() and getFakeAge() access this.#age identically but have different results. If you have been developing with JavaScript for a long time, this should throw you off with the livid horror. What in the world!

The private #age property is stored differently from the original context with this. While it has its own unique space for each instance, additionally, it has a separate space within the class. To put it simply, the Human class scope's #age and Person class scope's #age are distinctively different. Therefore, when the getAge() of Human class is ran, it accesses the Human's #age, and when the Person's getFakeAge() is ran, it accesses the Person's #age. This is what I mean by "all private fields will have its own unique scope within the located class."

Overall, the specs have not deviated too far from the conceptual idea of a private field. However, the last notation may lead to uninteded errors if you don’t fully understand the differences in scoping, so let us all be extra careful.

Closing Remarks

When I first found the specs in TC39, I had no crap to give about the private anything. Perhaps it is because I spent too much time developing in an environment where private does not mean much, but I have not been feeling the need for it. Because the concept of privacy did not exist in the first place, I have made it a habit to be more careful about separating identifiers and aptly using closures. I may be that this is the more JavaScript way to do things. However, I am excited to see where the idea of privacy in JavaScript would take the world of classes and application designs. It is very much possible that JavaScript developers exploit this in an unintended way. May be the spec authors have opened the gates for a whole new level of exploitation. Anyways, so I leave you with the statement that “I like JavaScript.” And with that, I take a sip of my wine and stare nostalgically into the sunset.

--

--

TOAST UI
TOAST UI

No responses yet