Engrain by ByteRush

JS.04.03 Junior· 11 minCore — asked often

Classes: private fields, static members and inheritance, built on prototypes

class is a cleaner way to write constructors and prototypes, plus real private fields. Learn methods, #private, static, getters, extends, super and brand checks.

Comes up in: JavaScript, Recruiter screen, Low-level design

Learn#

In Nisha’s wallet app, a bug elsewhere does account.balance = 999999. Nothing stops it. Underscore names like _balance are only a polite request. She needs data that code outside the class truly cannot reach.

A class gives her that: a private field, written #balance, can only be used inside the class body. Outside, account.#balance is a syntax error.

Most of a class is the same machinery as constructors and prototypes, written more clearly.

The constructor runs on new. Methods go on the prototype and are shared.

A get balance() lets outsiders read without writing.

Static members belong to the class itself, not to instances; a static { } block runs once to set them up.

extends links the prototypes, and super calls the parent’s constructor or methods.

class Account {
  #balance = 0;
  static #count = 0;
  static { Account.bank = "Engrain Bank"; }
  constructor(owner) { this.owner = owner; Account.#count++; }
  deposit(amount) { this.#balance += amount; return this; }
  get balance() { return this.#balance; }
  static isAccount(x) { return #balance in x; }
  static get count() { return Account.#count; }
}
class Savings extends Account {
  constructor(owner, rate) { super(owner); this.rate = rate; }
  addInterest() { return this.deposit(this.balance * this.rate); }
}
const s = new Savings("Asha", 0.1).deposit(1000).addInterest();
console.log(s.balance, Account.isAccount(s), Account.isAccount({ balance: 5 }));
console.log(Object.keys(s), Account.count, Account.bank);

Output (verified)

1100 true false
[ 'owner', 'rate' ] 1 Engrain Bank

#balance in x is a brand check: true only for objects the class really created.

Building a Savings accountStep 1 of 4

constructors running

  • Savings

object s

  • (not created yet)

new Savings('Asha', 0.1): Savings must call super() before it can use this.

Read the steps as text
  1. new Savings('Asha', 0.1): Savings must call super() before it can use this.
  2. super('Asha') runs Account's constructor. The object is created here, with its private #balance = 0.
  3. Back in Savings, this is ready. rate is added.
  4. deposit and addInterest live on the prototypes, shared by every account. Only they can touch #balance.

Predict#

Predict first

class A { #x = 1; static read(o) { return o.#x; } } — what does A.read({}) do?

Interview angle#

Say it like this — about 30 seconds

“Classes are mostly a clearer syntax for constructor functions and prototypes: methods go on the prototype, extends links prototypes, and super calls the parent. But some parts are new. Classes must be called with new, their bodies are strict, and #private fields are enforced by the engine — not visible to Object.keys, JSON.stringify or proxies. Unlike closures, private fields are shared by methods on the prototype, and #x in obj gives a reliable brand check.”

Stays on your device. Nothing is uploaded.
Follow-up How do private fields differ from closures or _x names?
_x is only a naming convention. Closures hide data but need per-instance functions. #x is hidden by the engine and still lets shared prototype methods use it.
Follow-up Why must a subclass call super() before using this?
In a derived class, the parent constructor creates the object. Until super() returns, there is no this to use, so accessing it throws a ReferenceError.
Follow-up What is a static member for?
Data or helpers about the class as a whole — counters, factory methods like Account.fromJSON, or brand checks.

Practice#

On your own

Rewrite BankAccount with a private #balance, a read-only balance getter, a guard in withdraw, and a real brand check.

Done when

  • deposit and withdraw return the new balance; withdrawing too much throws.
  • acc.balance = 1e6 changes nothing, and the balance does not appear in JSON.stringify(acc).
  • BankAccount.isAccount(acc) is true; a look-alike object is false.
Your solution
Ctrl/⌘ + Enter

Remember#

Try to answer each card before you open it. Then add them to your reviews.

Where do class methods live?

On the class's prototype, shared by all instances.

What makes #private fields different from _names?

The engine enforces them; code outside the class cannot read or write them.

What does #x in obj check?

Whether obj was created with the class's private field (a brand check).

When must a subclass call super()?

Before using this in its constructor.

Go deeper

Four layers. Stop at the one your interview needs; each ends with a quick check.

The core ideaEvery round

Constructor sets up each instance; methods are shared on the prototype; #private data is invisible outside; static belongs to the class; extends/super link parent and child.

Check yourself: Does new Savings() have its own copy of deposit()?

No. deposit lives on Account.prototype and is shared.

How it works underneathSenior rounds

Private fields are not properties. Each object keeps them in an internal [[PrivateElements]] list, keyed by a unique Private Name that only code inside the class body can mention Source: ECMA-262: PrivateElement. So Object.keys, in (without #) and proxies never see them.

Check yourself: Where are private fields stored?

In an internal list on each object ([[PrivateElements]]), not as properties.

Edge cases and trapsWhere people slip

Calling a class without new throws. Class declarations are in the TDZ before their line. Methods are not enumerable. Private names are per class: a subclass cannot read its parent’s #balance Source: MDN: Private properties.

Check yourself: Can you call a class without new?

No. It throws a TypeError.

In productionStaff-level answers

Keep inheritance shallow — one level is usually enough — and prefer composition (JS.04.04). Use private fields for invariants like balances. In TypeScript, private is only a compile-time check; #private is enforced at run time (TS.04.04).

Check yourself: Why prefer composition over deep class trees?

Deep inheritance couples classes tightly and makes changes risky; composing small parts is easier to change.

Finished the five steps? Mark it, and its cards join your reviews.

Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

Interview questions this answers

  • How do private fields (#x) differ from closures or _x conventions?