Can you write this from memory?
Define a class `User`.
JavaScript classes are syntactic sugar over prototypes, but they have rules that cause real bugs.
Strict mode behaviors:
- Class bodies run in strict mode
- Class declarations behave like
let: they're block-scoped and in the Temporal Dead Zone (you can't use them before the declaration runs) - In derived constructors, you MUST call
super()before touchingthis
We focus on what trips developers up: passing methods as callbacks and losing this, correct extends/super usage, static factories, and private # fields that work nothing like regular properties.
- Class bodies run in strict mode, even if the surrounding code doesn't.
- Class declarations are block-scoped like
let. You can't use a class before its declaration (Temporal Dead Zone). - In derived constructors,
super()must come beforethis. The base class initializes the instance first.
Most class bugs trace back to forgetting one of these.
When you pass a method as a callback, it loses its this:
class Button {
constructor(label) {
this.label = label;
}
handleClick() {
console.log(`Clicked: ${this.label}`);
}
}
const btn = new Button('Submit');
const fn = btn.handleClick;
fn(); // TypeError: Cannot read property 'label' of undefined
Why it happens: Assigning the method to a variable and calling it detaches it from btn. In strict mode (which class bodies always use), a bare function call sets this to undefined.
The same problem appears with setTimeout(btn.handleClick, 0). With addEventListener, the result is different but still broken: the browser sets this to the DOM element, not your class instance, so this.label silently returns undefined instead of throwing.
Fix 1: bind() in the constructor
class Button {
constructor(label) {
this.label = label;
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(`Clicked: ${this.label}`);
}
}
Pros: Method stays on prototype (shared across instances). Cons: Verbose; easy to forget.
Fix 2: Arrow function class field
class Button {
label;
handleClick = () => {
console.log(`Clicked: ${this.label}`);
};
constructor(label) {
this.label = label;
}
}
Pros: Automatic binding; cleaner syntax. Cons: Creates a new function per instance (not shared on prototype). With thousands of instances, this matters for memory.
Rule of thumb: Use arrow fields for event handlers and callbacks. Use regular methods for internal logic called via this.method(). This callback-passing pattern is central to the Observer design pattern, where objects register handlers to be notified of state changes.
Private fields (#field) are truly private, not by convention but by language enforcement:
class BankAccount {
#balance = 0;
deposit(amount) {
if (amount > 0) this.#balance += amount;
}
get balance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
console.log(account.balance); // 100
console.log(account.#balance); // SyntaxError!
Key differences from regular properties
| Regular properties | Private fields |
|---|---|
| Can be added anytime | Must be declared in class body |
| Enumerable with Object.keys() | Not enumerable |
| Accessible via bracket notation | No dynamic access |
| Work with Proxy | Don't work with Proxy |
| Can be deleted | Cannot be deleted |
Checking for private fields (brand checks)
Sometimes you need to check if an object has your private field (for example, in a static method that accepts any object):
class MyClass {
#internal = 0;
static isInstance(obj) {
return #internal in obj; // "ergonomic brand check"
}
}
MyClass.isInstance(new MyClass()); // true
MyClass.isInstance({}); // false
This is cleaner than instanceof when you care about the internal structure, not the prototype chain.
Inheritance lets you build specialized classes from general ones. When subclasses differ mainly in behavior (not structure), consider the Strategy design pattern as a composition-based alternative:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // MUST call before using this
this.breed = breed;
}
speak() {
console.log(`${this.name} barks`);
}
}
The super() requirement
In a derived class constructor, you cannot use this until super() is called:
class Broken extends Animal {
constructor(name) {
this.extra = 'oops'; // ReferenceError!
super(name);
}
}
Why? The base class constructor initializes the instance. Until it runs, this doesn't exist yet.
Calling overridden methods with super.method()
class Cat extends Animal {
speak() {
super.speak(); // Call parent's speak()
console.log('...meow');
}
}
Static methods belong to the class itself, not instances:
class User {
constructor(id, name) {
this.id = id;
this.name = name;
}
// Factory method: validates input, returns instance
static fromJSON(json) {
const data = typeof json === 'string' ? JSON.parse(json) : json;
if (!data.id || !data.name) {
throw new Error('Invalid user data');
}
return new this(data.id, data.name); // "this" allows subclass inheritance
}
// Utility method: doesn't need instance data
static isValidId(id) {
return typeof id === 'number' && id > 0;
}
}
const user = User.fromJSON('{"id": 1, "name": "Alice"}');
When to use static:
- Factory methods (
fromJSON,create,clone) -- this approach is formalized in the Factory design pattern - Utility functions that don't need
this - Constants or configuration
Use getters/setters to expose private state safely:
class Temperature {
#celsius = 0;
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (value < -273.15) throw new Error('Below absolute zero');
this.#celsius = value;
}
get fahrenheit() {
return this.#celsius * 9/5 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9; // Uses the celsius setter
}
}
Pattern: Private field + public getter + validating setter = bulletproof encapsulation.
| Pattern | Code |
|---|---|
Fix this with bind | this.method = this.method.bind(this) |
Fix this with arrow | method = () => { ... } |
| Private field | #field = value |
| Brand check | #field in obj |
| Static factory | static fromJSON(obj) { return new this(...) } |
| Call parent method | super.method() |
When to Use JavaScript Classes
- Model entities with state + behavior where invariants belong on methods/getters.
- Create readable APIs (static factories, well-named methods) instead of "bags of functions".
- Use inheritance sparingly for true "is-a" relationships; prefer composition when behavior differs.
- Encapsulate internals with private # fields and expose only safe methods.
Check Your Understanding: JavaScript Classes
Implement a class with a private field, a getter, and a static factory (fromJSON) that validates input.
Declare the private field with # in the class body, validate in fromJSON, construct via new, and expose read access via a getter.
What You'll Practice: JavaScript Classes
Common JavaScript Classes Pitfalls
- Using a class before its declaration (TDZ / "non-hoisted" behavior)
- Forgetting super() in a derived constructor before accessing this
- Losing this when passing methods as callbacks (setTimeout, event handlers, etc.)
- Using arrow field methods everywhere (per-instance functions; prototype sharing lost)
- Assuming private # fields are "just properties" (they can't be enumerated/proxied/dynamically accessed)
- Trying to create a private field by assignment instead of declaring it in the class body
JavaScript Classes FAQ
Are classes hoisted?
Not like function declarations. Class declarations are block-scoped and behave like let: you can't access them before the declaration is evaluated (Temporal Dead Zone).
Why must I call super() before using this in a subclass?
In a derived constructor, the base class must run first to initialize the instance. JavaScript requires super() before you can access this.
Why does "this" break in class methods?
If you pass a method as a callback (e.g. setTimeout(obj.method, 0)), it's called without the original receiver object, so this becomes undefined (in strict mode) or the global object. Fix with method.bind(this) in the constructor, or with an arrow function stored on the instance.
When should I use an arrow function as a class field?
Use it when you frequently pass the method as a callback and want this to stay bound to the instance. Tradeoff: the function is created per instance rather than shared on the prototype, so it uses more memory with many instances.
How do private fields (#) work?
Private fields must be declared in the class body and can't be created later via assignment. Accessing them outside the class is a syntax error, and they aren't normal properties: you can't enumerate, proxy, or access them dynamically.
Can I check whether an object has a private field?
Yes: use the #name in obj syntax (e.g. #x in obj) inside the class that declares it. This is called an "ergonomic brand check."
What are public class fields?
Public class fields (field = value syntax) are instance properties created when the constructor runs. They're enumerable and configurable by default, unlike prototype methods.
When should I use static methods vs instance methods?
Use static for utility functions that don't need instance data (e.g., Math.max) and factory methods (fromJSON, create). Use instance methods when behavior needs access to this.
JavaScript Classes Syntax Quick Reference
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}class Timer {
constructor() {
this.count = 0;
this.tick = this.tick.bind(this);
}
tick() {
this.count += 1;
}
}
const t = new Timer();
setInterval(t.tick, 1000);class Timer {
count = 0;
tick = () => {
this.count += 1;
};
}
const t = new Timer();
setInterval(t.tick, 1000);class Dog extends Animal {
constructor(name, breed) {
super(name); // Must call before using this
this.breed = breed;
}
speak() {
console.log(`${this.name} barks`);
}
}class Counter {
#value = 0;
inc() { this.#value += 1; }
get value() { return this.#value; }
}class User {
constructor(id, name) {
this.id = id;
this.name = name;
}
static fromJSON(obj) {
if (!obj || typeof obj.id !== "number") {
throw new TypeError("Invalid user JSON");
}
return new this(obj.id, obj.name); // "this" = subclass-friendly
}
}class Auth {
#token = null;
login(password) {
if (this.#validate(password)) {
this.#token = crypto.randomUUID();
}
}
#validate(password) {
return password.length >= 8;
}
}class MyClass {
#secret = 42;
static isMyClass(obj) {
return #secret in obj;
}
}JavaScript Classes Sample Exercises
Define a method `greet` that returns "Hi".
greet() {
return "Hi";
}
Create an instance of `User` passing "Alice" and store it in `user`.
const user = new User("Alice");Fill in the FIRST blank only - the keyword that defines a class.
class+ 21 more exercises