JavaScript Journey Towards Encapsulation

2020. 3. 12.

JavaScript Journey Towards Encapsulation

Among the ECMAScript class field specifications, there is Private field, also known as Private Property (hereinafter referred to as Private Property). The class field spec has reached Stage 3 (Candidate), so it will likely soon pass Stage 4 (Finished) and become a standard specification. Honestly, when I first saw the specification document, I had mixed feelings: excitement ("Are we finally getting private?") and disappointment ("The syntax looks a bit off"), but it didn't feel quite real. Time passed, and I forgot about it until I recently heard that TypeScript 3.8 officially supports it. This prompted me to properly learn about Private Properties. Of course, I plan to actively use them in projects now. Public class fields have already been very useful for a long time, thanks to the Babel plugin babel-plugin-proposal-class-properties.

The Sorrow of Lacking Privacy

In JavaScript, where creating private properties for objects wasn't possible, several alternatives have been used. Since it was fundamentally impossible to make properties private like in other class-based languages, conventions were established, or workarounds providing similar effects were employed.

The most common convention-based method is using the _ (underscore) prefix for property names. This method is also used in Python.

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

To make the example look older and more cumbersome, I used a function constructor instead of the class keyword.

This method only treated properties as private by convention; they were actually public and could be accessed from anywhere outside. However, I believe the agreement that fields or methods prefixed with _ should not be used externally significantly contributed to code readability. Just like thoughtlessly using i for the index variable in a for loop, as long as it was used consistently according to the agreement, it was a quite useful method. Douglas Crockford (hereinafter Douglas) argued on his blog that this method should be avoided because it could mislead people into thinking non-private fields behave as private. However, I think it was a decent approach if the agreement among developers was clear and consistently maintained. After JSDoc became widespread, editors increasingly provided various information and convenience features using JSDoc tags within the code, beyond its original purpose of automating documentation generation. JSDoc became not only a documentation automation tool but also an extension syntax that overcame the language's expressive limitations through comments. JSDoc used the @private tag to indicate that a member was private. Isn't this much better than _, being more explicit and automatically generating documentation? The JavaScript community gradually stopped using _. For the same reason, I also agreed with prohibiting the use of _ by convention.

A method to create fundamentally inaccessible private properties involves using closures. This was also the method Douglas suggested instead of _. As JavaScript often does, there might be other strange or revolutionary methods, but using closures is the most common. In fact, it's a good example of appropriately using closures (Reference: Closures, Encapsulation, and Information Hiding).

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

  this.doSomethingWithPrivateProp = () => { /* ... */ }
}

Since the syntax for accessing data differs from using this, mixing it with the this context could reduce readability due to inconsistent code style, but it effectively isolated data. The isolation was such that it seemed to separate dimensions from the instance context. This approach is useful not only for hiding data but also for hiding methods. This characteristic was used to implement the commonly known module pattern.

function SomeModule() {
  const privateProp = 'dont touch this';
  const publicProp = 'you can touch this';

  const _doSomethingWithPrivateProp = () => { /* ... */ }

  const publicMethod = () => {
    _doSomethingWithPrivateProp();
    // ...
  }

  return {
    publicProp,
    publicMethod
  }
}

The module pattern is still useful in certain parts (like high-level interfaces or situations where ES6 Modules cannot be used), but with the advent of ES6 Modules (hereinafter ESM), it gradually disappeared from FE project codebases. I honestly can't remember the last time I actually used the module pattern in development. It's been at least five years. In fact, this type of module pattern is the precursor to ESM, and they aim to solve the same problem. Even if you look at the transformed code of ESM generated by Webpack, you'll see methods similar to the traditional module pattern being used. The thirst for encapsulation, like private, was somewhat quenched by ESM, but there wasn't much change when needing to create private data for each instance context of a constructor. The direction was fundamentally different from the start.

Using Symbol allows for creating private properties with a slightly more ECMAScript-like workaround. Actually, rather than a workaround, I think it's the optimal method using ES6 resources appropriately. I once thought about it and felt it was a brilliant idea, though I haven't used it in practice yet. And well, now that private is officially supported...

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, the symbol can be used to access the field or method, but unless the symbol is exported, there's no way to access it from the outside. Because you don't know the name of the field to access. This method isolates the property name into a different dimension.

Nice Sharp (#)

Anyway, finally, JavaScript now has a way to create private properties in classes in the true sense, not as a workaround but as a standard feature provided by the language. Based on the TC39 specification document, here's a brief summary of its features:

  • It's a Stage-3 specification, meaning it will become a standard spec unless there are significant objections. Of course, there's still room for changes or improvements.
  • It doesn't use keywords like private. Instead, it uses the # (hash/sharp) prefix. It's a prefix, not a keyword. A field prefixed with # behaves as a Private field.
  • It's part of the Class Field Declarations spec. A difference from public fields is that they can only be created through class field declarations. This means you cannot dynamically add private fields to an object.
  • It has limitations regarding methods. It cannot be used with method declarations. To create a private method, you must define it as a function expression.
  • Computed Property Names cannot be used. Only #foo itself is allowed as an identifier; #[fooName] is a syntax error.
  • All Private fields have a scope unique to their containing class. This leads to a distinctive characteristic (discussed later).

It's not yet the level of support found in typical class-based languages. There are some constraints, but it's still stage-3, and there's always potential for updates or improvements. I am curious why private methods weren't discussed together from the beginning.

Let's try using it. (Using the TypeScript compiler to check error messages, but the example code uses only ECMAScript syntax).

class Human {
  #age = 10;
}

const person = new Human();

Simply using the # prefix, I created a property named #age in the Human class.

Let's try accessing it directly to see if it's truly private.

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

As expected, an error message indicating the property cannot be accessed outside the class is displayed.

And as mentioned earlier, # is not a keyword but a prefix of the property name.

class Human {
  #age = 10;

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

You cannot access it without the #. Accessing it without part of the identifier's name means accessing a non-existent property.

Now let's correctly define and use a private property within the class.

class Human {
  #age = 10;

  getAge() {
    return this.#age;
  }
}

const person = new Human();

console.log(person.getAge()); // 10

A getter getAge() is exposed externally to allow access to the #age value.

It's obvious, but a property made private is inaccessible from anywhere except the class where the private property is defined. It's also inaccessible in subclasses. I mention this, obvious as it may be, for those who might suspect JavaScript couldn't be this normal.

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.
  }
}

In Person, which inherits from Human, the private property #age of Human cannot be accessed.

However, there's a slightly peculiar characteristic. It's not the peculiarity of the private property itself, but rather a characteristic that seems particularly unique because it's JavaScript. And this characteristic arises from the point summarized earlier: "All Private fields have a scope unique to their containing class."

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 example above uses no private properties. In the Person object inheriting Human, age is declared again, and a getter with a different name getFakeAge() is defined. With public properties, since there is only one age property in the this context, the value of age is 20. This is the same whether executing Human's getAge() or Person's getFakeAge(), resulting in 20. This is because there is only one age in the instance context pointed to by this.

Now let's change age to the private property #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

The results of getAge() and getFakeAge(), both accessing this.#age, are different. For those who have developed in JavaScript for a long time, this might induce shock, horror, and confusion. What is this?

The private property #age is stored differently from the this context we've known. While it retains an independent space per instance like before, it additionally has an independent space per class. Simply put, the #age in the Human class scope and the #age in the Person class scope are different. Therefore, when getAge(), belonging to the Human class, is executed, it accesses Human's #age, and when Person's getFakeAge() is executed, it accesses Person's #age. That's precisely the meaning of the statement "All Private fields have a scope unique to their containing class."

Overall, it doesn't deviate much from the concept of Private fields, but the last point discussed could lead to hard-to-find errors in specific situations if not properly understood. Be careful.

Finally

When I first discovered the spec at TC39, I didn't pay much attention to it, being indifferent to the concept of private. Perhaps because I had been developing for a long time in an environment dealing with objects that lacked privacy, I didn't feel a strong need for it. The absence of such a concept ingrained in me the habit of separating identifiers more meaningfully, handling them carefully, and utilizing closures appropriately. In some ways, that was clearer (in a JavaScript sense). I look forward to seeing how the concept of private will influence class and application design in the JavaScript ecosystem. JavaScript developers will likely find ways to misuse private properties for other workarounds(?). You could say it opened up new possibilities for workarounds. Anyway, this is why I like JavaScript. I conclude, gazing lonely at the sunset with a glass of wine and a wry smile.

♥ Support writer ♥
with kakaopay

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

shiren • © 2025Sungho Kim