Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ES2022+ ~27 min· topic 6 of 8

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.

account.jswhole filejavascript
class Account {
  balance = 0;
}
const acc = new Account();
acc.balance = -500; // nothing stops this

02Make 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.

account.jswhole filejavascript
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.

account.jswhole filejavascript
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");
  }
}
Change it only through checked methodsdiagram
Rendering diagram…

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.

terminal
$ console.log(acc.#balance)
── expected output ──
Uncaught SyntaxError: Private field '#balance' must be declared in an enclosing class

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.

account.jswhole filejavascript
class Account {
  #balance = 0;
  static isAccount(obj) {
    return #balance in obj;
  }
}
Account.isAccount(new Account()); // true
Account.isAccount({ balance: 0 }); // false

Try it yourself

  1. 1

    Add a withdraw method

    In the first example, add withdraw(amount) that throws RangeError("Not enough money") when amount is more than this.#balance. Call acc.withdraw(800) inside try/catch and log the error. Predict the message.

  2. 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. 3

    Remove the object check

    In the second example, change isOrder to only return #items in value;. Predict what Order.isOrder(null) does now.

Code & diagrams

A bank account with a private balance New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

500
BankAccount { owner: 'Asha' }
[ 'owner' ] {"owner":"Asha"}
500 [ 'owner', '#balance' ]
Private methods, static private fields and the brand check New tab

Each order gets an id from a private static counter. `#items in value` only works on objects, so `isOrder` checks that first.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Order { id: 1 } Order { id: 2 }
45 50
undefined undefined
true false false
Reading a private field from the wrong object New tab

Code 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

Press Run to see the page.

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: TypeError
Underscore convention and closures, compared New tab

The 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

Press Run to see the page.

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.

terminal
$ Console
── what you'll see ──
Uncaught SyntaxError: Private field '#count' must be declared in an enclosing class

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.

terminal
$ Console
── what you'll see ──
Uncaught SyntaxError: Private field '#balance' must be declared in an enclosing class

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 obj are the robust replacement for instanceof for your own classes: they can't be fooled by Object.create(Class.prototype) or Symbol.hasInstance, though like instanceof they 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 get this = the proxy. Keep reactive state in plain objects, or call methods on the raw instance.

  • ▸

    Before ES2022, libraries got privacy with a WeakMap keyed 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. 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 = -500 and 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. 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 as this.#balance. The # is part of the name: #balance and balance are two different things, so you can have a private #balance and a public getter get 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. 3

    Private names are visible only inside the class body's text. Writing account.#balance anywhere outside the class is a SyntaxError: the script doesn't even start. Private fields don't show up in Object.keys, for...in, JSON.stringify, the spread operator {...obj} or Object.getOwnPropertyNames, and a property named with a string, like obj["#balance"], is a completely unrelated normal property.

  4. 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 only this, which is handy for methods like equals(other).

  5. 5

    Inside the class, reading obj.#field on 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 returns true only 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 write this.#parentField; it must use the parent's public or protected-by-convention methods.

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

01

What is encapsulation, and how do private fields support it?

02

How are #private fields different from _underscore properties and from closure variables?

03

What happens when class code reads obj.#x on an object that isn't an instance, and how do you check safely?

04

Can a subclass use its parent's private fields? What should it do instead?

Practice

01

Write a class Counter with a private #count, methods increment() and reset(), and a getter value. Show that Object.keys(new Counter()) is empty.

02

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.

03

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.

  • ↔

    #fields share methods through the prototype and are fast, while closures give privacy without classes but cost one function per object; both beat _underscore when 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 obj safely.

  • I know subclasses can't name the parent's private fields.

  • I can compare #private, _convention and closures.