Topic 7.6
Private Fields and Methods
In one line
Names that start with # (like #balance or #validate()) are private: only code written inside that class body can read or change them, and the engine enforces it, so the outside world must go through the methods you choose to make public.
Think of it like this
A cash machine (ATM). You can press buttons on the front: check balance, withdraw, deposit. You can't open the box and take the money out by hand. The buttons are the public methods; the cash box inside is the private data. Because the only way in is through the buttons, the machine can check every request ("Is the PIN right? Is there enough money?").
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Encapsulation
- Hiding an object's inner data and letting the outside change it only through methods that check the rules.
- Private field
- A property whose name starts with
#, readable and writable only by code inside the class body. - Private method
- A method whose name starts with
#, callable only from inside the class. - Public interface
- The methods and properties outside code is allowed to use.
- Brand check
- Testing whether an object really is an instance of a class with
#field in obj. - _convention
- Starting a name with
_to say "private, please don't touch". Nothing enforces it. - Proxy
- A wrapper object that intercepts operations on another object. Private fields don't work through it.
Step by step
01The problem: anyone can change the balance
With a public balance property, outside code can set any number, skipping your rules.
class Account {
balance = 0;
}
const acc = new Account();
acc.balance = -500; // nothing stops this02Make the data private
Rename it #balance. Now only methods inside the class can touch it. Add a getter so outside code can read the balance but not set it.
class Account {
#balance = 0;
get balance() {
return this.#balance;
}
}03Change it only through checked methods
deposit and withdraw are the only doors. Each checks the request first, using a private helper method that outside code can't call.
class Account {
#balance = 0;
get balance() {
return this.#balance;
}
deposit(amount) {
this.#check(amount);
this.#balance += amount;
}
withdraw(amount) {
this.#check(amount);
if (amount > this.#balance) throw new RangeError("Not enough money");
this.#balance -= amount;
}
#check(amount) {
if (!(amount > 0)) throw new RangeError("Amount must be positive");
}
}04Try to break in from outside
acc.#balance outside the class is a SyntaxError, so the whole script refuses to run. acc.balance = 1000000 has no setter to call: it's ignored in sloppy code and throws in strict code. The money stays safe.
05Check the brand before reading
A static method that receives "some object" can't assume it's a real Account. #balance in obj answers safely, without throwing.
class Account {
#balance = 0;
static isAccount(obj) {
return #balance in obj;
}
}
Account.isAccount(new Account()); // true
Account.isAccount({ balance: 0 }); // falseTry it yourself
- 1
Add a withdraw method
In the first example, add
withdraw(amount)that throwsRangeError("Not enough money")whenamountis more thanthis.#balance. Callacc.withdraw(800)insidetry/catchand log the error. Predict the message. - 2
Reach in from outside
In the first example, add
console.log(acc.#balance);at the very end. Predict: will the first four lines still print? Run it and read the error. (Remember what a SyntaxError does to the whole script.) - 3
Remove the object check
In the second example, change
isOrderto onlyreturn #items in value;. Predict whatOrder.isOrder(null)does now.
Code & diagrams
The private field doesn't appear in the console output, keys or JSON. Writing to `balance` and to the string key `"#balance"` can't reach it.
Preview
Console
Nothing logged yet.Expected console output
500
BankAccount { owner: 'Asha' }
[ 'owner' ] {"owner":"Asha"}
500 [ 'owner', '#balance' ]Each order gets an id from a private static counter. `#items in value` only works on objects, so `isOrder` checks that first.
Preview
Console
Nothing logged yet.Expected console output
Order { id: 1 } Order { id: 2 }
45 50
undefined undefined
true false falseCode inside the class may read `#code` on any `Secret`. A look-alike object or a Proxy wrapper isn't a real instance, so the read throws.
Preview
Console
Nothing logged yet.Expected console output
42
true
TypeError: Cannot read private member #code from an object whose class did not declare it
through a Proxy: TypeErrorThe underscore only asks politely. The closure really hides the PIN, but every vault gets its own copy of `check`; a `#pin` field gives the same privacy with shared prototype methods.
Preview
Console
Nothing logged yet.Expected console output
true [ '_pin' ]
true undefined [ 'check' ]Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a private field without declaring it
In a class, write constructor() { this.#count = 0; } without a #count; line in the body.
Break #2
A subclass reads the parent's private field
Write class Savings extends Account { addInterest() { this.#balance *= 1.05; } } where #balance is declared in Account.
Myth vs fact
Myth
_name makes a property private.
Fact
It is only a naming habit. The property is public, enumerable, appears in JSON and can be changed by anyone. Only #name is enforced.
Myth
Private fields are completely invisible.
Fact
Your code can't reach them from outside, but DevTools shows them when you inspect an object, so never think of them as a way to keep secrets from the user. Anything in the browser is visible to the person using it.
Myth
You can loop over private fields or copy them with spread.
Fact
Object.keys, for...in, JSON.stringify, {...obj} and structuredClone all ignore private fields. A clone made with spread loses them, and the clone isn't a real instance.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Engines store private fields like hidden properties keyed by a unique private name per class evaluation (V8 uses private symbols). Access is as fast as normal property access. Every evaluation of a class expression creates new private names, so instances of two evaluations of the same class factory can't read each other's fields.
- ▸
Brand checks with
#x in objare the robust replacement forinstanceoffor your own classes: they can't be fooled byObject.create(Class.prototype)orSymbol.hasInstance, though likeinstanceofthey don't work across realms. Built-ins use internal slots the same way:Map.prototype.get.call({})throws because{}lacks the[[MapData]]slot. - ▸
Private fields and Proxies don't mix: frameworks that wrap objects in proxies for reactivity (such as Vue's
reactive()) can break classes with#fields, because methods called through the proxy getthis= the proxy. Keep reactive state in plain objects, or call methods on the raw instance. - ▸
Before ES2022, libraries got privacy with a
WeakMapkeyed by instance (privates.get(this).balance), which also lets garbage collection free the data with the object. You'll still see it in older code and in transpiled output for old browsers.
Remember this
- 1
Encapsulation means keeping an object's inner data hidden and letting the outside change it only through methods that check the rules. Without it, any code can write
account.balance = -500and break your program. JavaScript used to fake privacy with a naming convention,_balance("please don't touch"), but anyone could still read and change it. - 2
Private fields (ES2022) give real, engine-enforced privacy. Declare them in the class body with a
#in front:#balance = 0;. Use them inside the class asthis.#balance. The#is part of the name:#balanceandbalanceare two different things, so you can have a private#balanceand a public getterget balance()that returns it. Every private field must be declared in the body before use; you can't add new ones later with assignment like normal properties. - 3
Private names are visible only inside the class body's text. Writing
account.#balanceanywhere outside the class is a SyntaxError: the script doesn't even start. Private fields don't show up inObject.keys,for...in,JSON.stringify, the spread operator{...obj}orObject.getOwnPropertyNames, and a property named with a string, likeobj["#balance"], is a completely unrelated normal property. - 4
You can make private methods (
#validate(amount) { ... }), private getters/setters (get #fee() { ... }), static private fields (static #nextId = 1;) and static private methods. Use private methods for helper steps that outside code shouldn't call. Code inside the class can read the private fields of any instance of the same class, not onlythis, which is handy for methods likeequals(other). - 5
Inside the class, reading
obj.#fieldon an object that wasn't created by this class throws a TypeError (Cannot read private member #field from an object whose class did not declare it). To test safely first, use the brand check#field in obj(ES2022), which returnstrueonly for real instances. Private fields are not inherited for access: a subclass (Topic 7.7) has the parent's private fields on its instances, but its own code can't writethis.#parentField; it must use the parent's public or protected-by-convention methods. - 6
Privacy has costs. Debugging tools like Chrome DevTools still show private fields when you inspect an object (that's allowed: tools aren't your code), but your tests can't reach them, so test through the public methods. A **
Proxy(a wrapper object) breaks private access, because the proxy isn't the real instance. An older way to get privacy is a closure** (Topic 3.6): variables inside a factory function that only its returned methods can see.
Explain it without notes
What is encapsulation, and how do private fields support it?
How are #private fields different from _underscore properties and from closure variables?
What happens when class code reads obj.#x on an object that isn't an instance, and how do you check safely?
Can a subclass use its parent's private fields? What should it do instead?
Practice
Write a class Counter with a private #count, methods increment() and reset(), and a getter value. Show that Object.keys(new Counter()) is empty.
Write a class Password that stores a private #hash (use a simple stand-in: the reversed string) set in the constructor, and a method matches(text). It must never expose the stored value.
Add a static method Counter.isCounter(value) to your Counter that uses a brand check and never throws, even for null or 5.
Trade-offs
- ↔
Private fields protect invariants and let you refactor internals freely, but tests, subclasses and debugging helpers can't reach them; keep the public interface rich enough to test through.
- ↔
#fieldsshare methods through the prototype and are fast, while closures give privacy without classes but cost one function per object; both beat_underscorewhen privacy really matters.
Done when you can
I can declare and use private fields, private methods and static private members.
I can expose private data read-only with a getter.
I know outside access is a SyntaxError and wrong-object access is a TypeError.
I can use the brand check
#field in objsafely.I know subclasses can't name the parent's private fields.
I can compare
#private,_conventionand closures.