In modern JavaScript (ES2022+), Private Fields are a major language feature that allows developers to truly encapsulate data within a class. Unlike the traditional convention of using an underscore (_variable) to hint that a property is internal, private fields are enforced by the JavaScript engine itself.
Private fields are defined using the hash (#) prefix. Once a property is marked with a hash, it cannot be accessed, modified, or even detected from outside the class’s curly braces.
1. Syntax and Core Implementation
To declare a private field, you must define it at the top level of the class body. You cannot create private fields on the fly inside a constructor without first declaring them.
class BankAccount {
// 1. Declare the private field
#balance = 0;
constructor(owner, initialDeposit) {
this.owner = owner;
this.#balance = initialDeposit;
}
// 2. Private data is accessible inside class methods
getAccountSummary() {
return `${this.owner} has a balance of $${this.#balance}`;
}
}
const myAccount = new BankAccount("Alice", 1000);
console.log(myAccount.getAccountSummary()); // "Alice has a balance of $1000"
// console.log(myAccount.#balance); // ❌ SyntaxError: Private field '#balance' must be declared in an enclosing class
2. Hard Encapsulation vs. Conventions
The primary difference between # and _ is how the engine handles them.
- Hard Privacy: Private fields are not included in Object.keys(), for…in loops, or JSON.stringify(). They cannot be accessed even through “backdoor” methods like account[“#balance”].
- The “In” Operator: You can check for the presence of a private field using the in operator, which helps prevent errors when interacting with other instances of the same class.
class Secure {
#secret = "Top Secret";
static check(obj) {
return #secret in obj; // Returns true if obj is an instance of Secure
}
}
3. Private Methods and Accessors
Privacy isn’t limited to data variables. You can also make methods and getters/setters private to hide internal logic and “helper” functions from the user.
class CoffeeMachine {
#waterAmount = 0;
// Private method: Internal logic hidden from the user
#checkWater() {
if (this.#waterAmount <= 0) throw new Error("Out of water!");
}
fill(amount) {
this.#waterAmount += amount;
}
brew() {
this.#checkWater(); // Accessing internal private method
console.log("Brewing your coffee...");
this.#waterAmount -= 10;
}
}
const machine = new CoffeeMachine();
machine.fill(100);
machine.brew(); // Works
// machine.#checkWater(); // ❌ Error: Private method is not accessible
4. Inheritance and Private Fields
A key nuance of private fields is that they are not inherited by subclasses. Even though a child class extends a parent, it does not have access to the parent’s # fields. This ensures that a parent class can change its internal implementation without accidentally breaking subclasses that might have tried to rely on those internals.
class Parent {
#internalId = 123;
}
class Child extends Parent {
showId() {
// console.log(this.#internalId); // ❌ Error: Property '#internalId' does not exist on type 'Child'
}
}
5. Use Case: Protecting Internal State
Private fields are ideal for classes that manage complex states, such as a “State Store” or an “API Client,” where exposing internal counters or tokens could lead to bugs if modified by outside scripts.
class APIClient {
#apiKey;
#requestCount = 0;
constructor(key) {
this.#apiKey = key;
}
fetchData(endpoint) {
this.#requestCount++;
return fetch(`${endpoint}?key=${this.#apiKey}`);
}
get usage() {
return this.#requestCount; // Expose the count, but not the key
}
}
