A class combines construction with methods. new Counter(2) creates an instance, runs its constructor, and gives it access to methods on Counter.prototype. Each instance has its own fields; methods declared normally are shared through the prototype.
A field beginning with # is JavaScript private state, not a naming convention. Only the declaring class body can access it. A method using a private field also requires an instance carrying that private field; borrowing it onto an unrelated object fails. Public properties and private fields are different mechanisms.
Use a class when instances with behavior and a lifecycle make your API clearer. A closure is still a valid alternative. Prefer composition—holding another collaborator—over inheritance when you only need to reuse a capability. This library’s custom error will extend Error because it truly is an error; other helpers need no inheritance hierarchy.
Avoid arrow instance fields just to make every method automatically bound: that allocates a separate function per instance. Choose an explicit callback closure where needed.
Predict, run, and explain
Write the output and trace the important state changes before checking it. Then run the program. Each editor is independent. Async examples end with main() so this editor waits for their work.
class Counter {
#value;
constructor(start = 0) {
this.#value = start;
}
add(amount) {
this.#value += amount;
return this.#value;
}
}
const first = new Counter();
const second = new Counter(10);
console.log(first.add(2));
console.log(second.add(1));
console.log(first.add === second.add);Check your prediction
2
11
true Read a getter like a property
A getter runs a function when a property is read. Inside a class, get current() { return this.#value; } exposes the value as counter.current, without parentheses. It does not create a separate stored copy. With no setter, callers cannot assign through that property, but methods can still change the private field. Use this syntax in the exercise below.
Choose where state and behavior live
An instance owns its fields; ordinary class methods are looked up on the prototype. Sharing the method does not mean sharing its state. The receiver identifies which instance’s fields the method uses on each call.
Private fields restrict access to code in the declaring class. They do not automatically make instances immutable or bind their methods. Compare a private-field class with the closure factories from intermediate: both can own state, and the useful choice depends on the public API you need.
class Counter {
#count = 0;
next() {
return ++this.#count;
}
get current() {
return this.#count;
}
}
const a = new Counter();
const b = new Counter();
a.next();
a.next();
console.log(a.current, b.current);
console.log(a.next === b.next);Check your prediction
2 0
true The method function is shared, while the two private counts are independent. The getter is accessed as a property; invoking a.current() would try to call a number.
class Counter {
#count = 0;
next() {
return ++this.#count;
}
}
const counter = new Counter();
// Reading the method does not keep counter as its receiver.
const detached = counter.next;
try {
// This call has no receiver, so accessing #count throws.
detached();
} catch (error) {
console.log(error instanceof TypeError);
}
// Calling through counter supplies the receiver and updates its count.
console.log(counter.next());Check your prediction
true
1 A private field cannot be read from a missing or unrelated receiver. The failed detached call does not increment the valid instance; a later proper method call still starts at one.
Try the rule on new inputs
Write the same small counter as a closure factory and as a class. Compare receiver requirements, instance state and public methods. Do not introduce inheritance merely because you chose class syntax.
Your turn
Classes & Private State: practise
Implement a Counter with a private numeric field starting at zero, increment() returning the new value, and a getter current. Print 1, 2 and 0 using two independent counters.
// A getter is declared as get current() { ... } and read without parentheses.Hint 1: the idea
The field belongs to each instance; the method lives on the prototype.
Hint 2: a first step
Initialize #value = 0 in the class body.
Show one solution
class Counter {
#value = 0;
increment() {
this.#value += 1;
return this.#value;
}
get current() {
return this.#value;
}
}
const first = new Counter();
const second = new Counter();
console.log(first.increment());
console.log(first.increment());
console.log(second.current);1
2
0Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?
Change it and test it
Try reading #value outside the class: this is a syntax error, so restore the code afterward. Then pass () => first.increment() as a callback. Explain why private state does not make an object immutable or make a callback automatically bound.
Completion is saved in this browser only. It does not measure mastery.