Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. JavaScript
  3. JavaScript Classes Practice
JavaScript24 exercises

JavaScript Classes Practice

Practice JavaScript class syntax with real gotchas: constructors, public class fields, private # fields, extends/super, static factories, getters/setters, and fixing "this" binding bugs.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Define a class `User`.

On this page
  1. 1Three rules that prevent most class bugs
  2. 2The "this" binding problem (and how to fix it)
  3. Fix 1: bind() in the constructor
  4. Fix 2: Arrow function class field
  5. 3Private fields: not just "underscore convention"
  6. Key differences from regular properties
  7. Checking for private fields (brand checks)
  8. 4Inheritance: extends and super
  9. The super() requirement
  10. Calling overridden methods with super.method()
  11. 5Static methods and factory patterns
  12. 6Getters and setters: controlled access
  13. 7Quick reference
  14. 8References
Three rules that prevent most class bugsThe "this" binding problem (and how to fix it)Private fields: not just "underscore convention"Inheritance: extends and superStatic methods and factory patternsGetters and setters: controlled accessQuick referenceReferences

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 touching this

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.

Related JavaScript Topics
JavaScript FunctionsJavaScript Modules & ScopeTypeScriptJavaScript Error Handling

Class bodies are always strict mode, class declarations have a TDZ like let, and derived constructors must call super() before touching this.

  1. Class bodies run in strict mode, even if the surrounding code doesn't.
  2. Class declarations are block-scoped like let. You can't use a class before its declaration (Temporal Dead Zone).
  3. In derived constructors, super() must come before this. The base class initializes the instance first.

Most class bugs trace back to forgetting one of these.


Ready to practice?

Start practicing JavaScript Classes with spaced repetition

Passing a method as a callback loses this. Fix with this.method = this.method.bind(this) in the constructor, or declare the method as an arrow class field.

Three ways a class method can be called: direct method call (this = btn), bare function call (this = undefined), addEventListener (this = element). Fixes: bind in constructor or arrow class field

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.


#field is language-enforced privacy—accessing it from outside throws a SyntaxError. Unlike _field, it can't be read, iterated, or proxied from outside the class.

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 propertiesPrivate fields
Can be added anytimeMust be declared in class body
Enumerable with Object.keys()Not enumerable
Accessible via bracket notationNo dynamic access
Work with ProxyDon't work with Proxy
Can be deletedCannot 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.


Prototype chain: dog instance links to Dog.prototype, then Animal.prototype, then Object.prototype, then null

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.


PatternCode
Fix this with bindthis.method = this.method.bind(this)
Fix this with arrowmethod = () => { ... }
Private field#field = value
Brand check#field in obj
Static factorystatic fromJSON(obj) { return new this(...) }
Call parent methodsuper.method()

  • MDN: Classes
  • MDN: Private class features
  • MDN: static
  • TC39: Ergonomic brand checks

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

Prompt

Implement a class with a private field, a getter, and a static factory (fromJSON) that validates input.

What a strong answer looks like

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

Class declarations and expressionsConstructors and instance methodsPublic class fields (field initializers)Private # fields and private methodsInheritance with extends + overriding methodssuper() calls in constructors and methodsStatic methods/properties + static factory methodsGetters and setters (public API over private state)Fixing lost this: bind() vs arrow fields

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

Basic class (constructor + method)
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a sound`);
  }
}
Fix lost this with bind()
class Timer {
  constructor() {
    this.count = 0;
    this.tick = this.tick.bind(this);
  }
  tick() {
    this.count += 1;
  }
}
const t = new Timer();
setInterval(t.tick, 1000);
Arrow class field (callback-safe, per instance)
class Timer {
  count = 0;
  tick = () => {
    this.count += 1;
  };
}
const t = new Timer();
setInterval(t.tick, 1000);
Inheritance + super()
class Dog extends Animal {
  constructor(name, breed) {
    super(name);  // Must call before using this
    this.breed = breed;
  }
  speak() {
    console.log(`${this.name} barks`);
  }
}
Private # field + getter
class Counter {
  #value = 0;
  inc() { this.#value += 1; }
  get value() { return this.#value; }
}
Static factory (fromJSON) with validation
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
  }
}
Private method (internal helper)
class Auth {
  #token = null;
  login(password) {
    if (this.#validate(password)) {
      this.#token = crypto.randomUUID();
    }
  }
  #validate(password) {
    return password.length >= 8;
  }
}
Private field brand check (#x in obj)
class MyClass {
  #secret = 42;
  static isMyClass(obj) {
    return #secret in obj;
  }
}

JavaScript Classes Sample Exercises

Example 1Difficulty: 1/5

Define a method `greet` that returns "Hi".

greet() {
  return "Hi";
}
Example 2Difficulty: 1/5

Create an instance of `User` passing "Alice" and store it in `user`.

const user = new User("Alice");
Example 3Difficulty: 1/5

Fill in the FIRST blank only - the keyword that defines a class.

class

+ 21 more exercises

Related Design Patterns

Observer PatternStrategy PatternFactory Pattern

Start practicing JavaScript Classes

Free daily exercises with spaced repetition. No credit card required.

← Back to JavaScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.