Private Fields
Until recently, JavaScript had no way to actually hide a property on an object — only conventions like a leading underscore. Private class fields, marked with a # prefix, fix that. They are truly inaccessible from outside the class body, even by reflection. Combined with classes and getters, they give you proper encapsulation.
The basic syntax
class Counter {
#count = 0; // declared and initialised — private
inc() { this.#count++; }
get value() { return this.#count; }
}
const c = new Counter();
c.inc(); c.inc(); c.inc();
console.log(c.value); // 3
try {
console.log(c.#count); // SyntaxError — outside the class
} catch (_) {}Note that #count is part of the identifier — there is no separate "private" keyword. You declare #name once at the top of the class, and refer to it as this.#name everywhere inside.
How private it actually is
Truly private — not just a convention. Reading or writing a private field from outside the class is a syntax error, not a runtime error you might catch.
Object.keys,for...in, and spread do not see private fields.JSON.stringifydoes not include them.You cannot reach them with
obj["#count"]— the brackets do not address private slots.Subclasses cannot read parent private fields directly. They are private to the class, not the instance.
Engines often implement private fields as a separate WeakMap behind the scenes.
Private vs underscore convention
The pre-private convention was to prefix "private" properties with an underscore — _count, _init. It was advisory only. Anything could still read or modify them, and they showed up in iteration.
Old convention — soft privacy
class Counter {
constructor() { this._count = 0; } // accessible from anywhere
inc() { this._count++; }
}
const c = new Counter();
c._count = 9999; // perfectly legal — no protectionModern — hard privacy
class Counter {
#count = 0;
inc() { this.#count++; }
}
const c = new Counter();
// c.#count = 9999; // SyntaxErrorStick with # for new code. The underscore convention is still common in older codebases — read it as "treat as private", but understand it is not enforced.
Private methods
Methods and static methods can also be private. Use them to break a long public method into helpers without exposing the helpers.
class Password {
#value;
constructor(plain) {
this.#value = this.#hash(plain);
}
check(plain) {
return this.#hash(plain) === this.#value;
}
// Pretend this is a real hash
#hash(s) {
return [...s].reduce((acc, ch) => acc + ch.charCodeAt(0), 0).toString(16);
}
}
const pw = new Password("hunter2");
console.log(pw.check("hunter2")); // true
console.log(pw.check("wrong")); // falseChecking with the in operator
The expression #name in obj works inside a class body and returns whether obj has that private field. It is the idiomatic way to check whether an object is actually an instance of this class — including against forged copies.
class Wallet {
#balance = 0;
static isWallet(value) {
try {
return #balance in value;
} catch {
return false;
}
}
}
const w = new Wallet();
console.log(Wallet.isWallet(w)); // true
console.log(Wallet.isWallet({})); // falsePrivate and inheritance
Private fields belong to the declaring class, not the chain. A subclass does not inherit them; if it declares #name of its own, that is a separate slot.
class Base {
#id = 1;
getId() { return this.#id; }
}
class Sub extends Base {
// this.#id // SyntaxError — Base's private, not visible here
getCustomId() { return this.getId(); } // ok via inherited method
}
console.log(new Sub().getCustomId()); // 1Static private members
class Counter {
static #total = 0;
static increment() { return ++Counter.#total; }
static get total() { return Counter.#total; }
}
Counter.increment();
Counter.increment();
console.log(Counter.total); // 2