Controversial, JavaScript classes aren’t real classes, they’re just syntactic sugar over prototypes.
But they make building objects way cleaner and less error-prone.
In this post we’ll walk through ES6 class basics, including declarations versus expressions, constructors and fields, private fields, instance versus static methods, and extends and super for inheritance.
You’ll get clear examples, what breaks, and small wins you can try in your editor.
By the end you’ll know when to use classes, how they map to prototypes, and how to avoid common gotchas.
Core Concepts of JavaScript Classes Explained

A JavaScript class is basically a blueprint for making objects. Write class Person and JavaScript quietly builds Person.prototype in the background. All those instance methods you define? They go straight onto that prototype object. Every object you spin up from the class shares those methods instead of lugging around its own copies. Here’s the thing: a class is still just a function under the hood. Run typeof Person and you’ll get “function” back, because classes are really syntactic sugar over JavaScript’s prototype system.
Constructors handle setup when you fire off new Person("Alice", 28). The constructor method runs first, slapping properties onto the new object using this. Don’t write a constructor? JavaScript gives you an empty one that just wires up the prototype chain. You can only have one constructor per class. Try adding a second and you’ll hit an error. The constructor needs new to work. Call it like a regular function and you’ll get a TypeError.
Instance methods sit on Person.prototype, not on each individual object. Call alice.greet() and JavaScript checks the object first, then climbs the prototype chain to Person.prototype and finds it there. Smart design. A thousand instances share one greet function instead of carrying a thousand copies. Class bodies automatically run in strict mode, so mistakes like forgetting new or assigning to undeclared variables blow up immediately.
Classes make object-oriented patterns simpler:
Structure. One declaration groups constructor, methods, and fields together instead of scattering them across function definitions and prototype assignments.
Shared methods. Instance methods automatically attach to the prototype. Every object uses the same function reference.
Readable syntax. The class keyword, method definitions, and constructor label make intent obvious.
Consistent instantiation. The new requirement stops accidental misuse and enforces proper object creation.
Understanding JavaScript Class Declarations and Expressions

Class declarations look like class Rectangle { ... } and act like let or const under the hood. They’re not hoisted. You can’t reference a class before its declaration shows up in the code. Try instantiating new Shape() before class Shape is defined and you’ll hit a ReferenceError because of the temporal dead zone. Different from function declarations, which hoist and work anywhere in their scope.
Class expressions let you assign a class to a variable. Either anonymously or with a name. Write const Circle = class { ... } and you’ve created an anonymous class stored in Circle. Or go with const Circle = class CircleClass { ... } to give the class an internal name you can use for recursion or debugging. Named class expressions work like named function expressions. The internal name is visible inside the class body but not outside. You can confirm a class was invoked correctly using new.target, which references the constructor being called and returns undefined if the class is invoked without new.
Declarations versus expressions:
Hoisting. Declarations aren’t hoisted. Expressions follow the same TDZ rules as let/const assignments.
Naming. Declarations require a name. Expressions can be anonymous or internally named.
Usage context. Declarations suit top-level definitions. Expressions work well for passing classes as arguments or returning them from factories.
Constructors, Fields, and Property Initialization in JavaScript Classes

The constructor method initializes new instances when you call new ClassName(...). JavaScript allows exactly one constructor per class. Add a second and you trigger a syntax error. Inside the constructor, you assign values to this.propertyName to create instance properties. Skip the constructor entirely? JavaScript generates a default empty one that links the prototype but assigns no properties. Constructors run before any other instance setup, making them the right place to validate arguments or set required state.
Class fields let you declare properties directly in the class body without writing them inside the constructor. Write class Product { id = 0; name = ""; } and it initializes id and name on every new instance before the constructor runs. Fields evaluate in the order they appear, and their values can reference this or call methods. Public fields are visible everywhere. Private fields use a # prefix to restrict access strictly to the class interior.
Fields initialized outside the constructor simplify default values and reduce boilerplate. But constructor initialization gives you more control when values depend on constructor arguments. Combining both patterns is common. Declare defaults as fields, then override them conditionally in the constructor if specific arguments are passed.
Private Fields
Private fields start with #. Like #secret or #apiKey. They exist only inside the class and can’t be read or written from outside, even indirectly. Try accessing instance.#secret from external code and you’ll hit a syntax error in strict mode or get undefined in non-strict contexts, depending on the environment. Private fields were standardized in ES2022 and provide true encapsulation without relying on naming conventions like _privateValue.
Private methods work the same way. Prefix the method name with # and it becomes invisible outside the class. You might use a private method like #validate() internally before saving data, then expose a public method save() that calls #validate() under the hood. Engines optimize private fields and methods to avoid the performance cost of dynamic property lookup, making them faster than simulated privacy patterns.
Instance Methods, Static Methods, and Method Resolution

Instance methods are functions defined inside the class body without the static keyword. They attach to ClassName.prototype and are available on every object created from the class. Call object.method() and JavaScript finds method on the prototype and invokes it with this bound to the calling object. Instance methods can access instance properties, other instance methods, and private fields. They represent behavior tied to individual objects.
Static methods belong to the class itself, not to instances. Declare static clean() { ... } and it puts clean directly on the class constructor. You call it with ClassName.clean() instead of instance.clean(). Static methods can’t access instance properties or this in the instance sense. They see this as the class constructor. Try calling a static method on an instance like obj.clean() and you’ll get a TypeError because clean doesn’t exist on the instance or its prototype. Static methods suit utility functions, factory helpers, or operations that manage class-level state like counters or caches.
Static versus instance methods:
Location. Instance methods live on ClassName.prototype. Static methods live directly on ClassName.
Invocation. Instance methods require an object (obj.method()). Static methods require the class (Class.method()).
Access. Instance methods can read instance properties and private fields. Static methods can’t touch instance data.
Inheritance. Both are inherited by subclasses, but static methods are called on the subclass constructor, not on instances.
Use cases. Instance methods model object behavior (calculate, render, validate). Static methods handle shared logic (parse, format, create).
Inheritance in JavaScript Classes Using extends and super

The extends keyword creates a subclass that inherits properties and methods from a parent class. Write class Developer extends Employee and it links Developer.prototype to Employee.prototype, forming a prototype chain. Call a method on a Developer instance and JavaScript searches the instance, then Developer.prototype, then Employee.prototype, and continues up the chain until it finds the method or reaches null.
Subclass constructors must call super() before using this. The super() call runs the parent constructor, initializing inherited properties. Try accessing this.name before calling super() and JavaScript throws a ReferenceError because the object isn’t fully constructed yet. You pass parent constructor arguments to super like super(name, age), and those values initialize the parent’s properties on the new object. After super() returns, you can add subclass-specific properties using this.
Method overriding happens when a subclass defines a method with the same name as a parent method. The subclass version shadows the parent version. Call developer.introduce() and it runs the Developer version, not the Employee version. You can still invoke the parent method inside the override using super.introduce(), letting you combine parent behavior with subclass customization. Overriding doesn’t modify the parent class. Employee instances still use the original method.
Calling super Correctly
The super() call must appear before any use of this in a subclass constructor. Forget super() entirely or place it after this.stack = stack and you trigger a runtime error: “Must call super constructor in derived class before accessing ‘this'”. This rule makes sure the parent constructor runs first, setting up the object’s foundational state before the subclass adds its own properties.
You must pass the correct number and type of arguments to super() based on what the parent constructor expects. If Employee requires name and role, calling super() with no arguments will leave those properties undefined. JavaScript doesn’t enforce parameter matching at the syntax level, so mismatched arguments surface as logic bugs rather than immediate errors.
Getters, Setters, and Encapsulation Patterns in JavaScript Classes

Getters and setters let you define property-like accessors that run code when reading or writing a value. A getter uses the syntax get propertyName() { return this.#value; }, and you access it like a regular property: object.propertyName. No parentheses, no method call syntax. Setters mirror this with set propertyName(newValue) { this.#value = newValue; }, and you assign to them like object.propertyName = 10. The setter must accept exactly one parameter. Write set propertyName(a, b) and you’ll throw a SyntaxError at parse time.
Getters and setters provide controlled access to private fields, letting you validate input, log changes, or compute derived values on the fly. A common pattern pairs a private field like #password with a public setter that encrypts the input before storing it: set password(plain) { this.#password = this.#encrypt(plain); }. You can make properties read-only by defining only a getter with no corresponding setter. Try assigning to a getter-only property and it silently fails in non-strict mode and throws a TypeError in strict mode.
Misspell a getter or setter and you create a new property instead of invoking the accessor, leading to silent bugs. Define set userName(value) but accidentally write object.username = "Alice" and JavaScript creates a separate username property without triggering your validation logic. TypeScript catches these at compile time by enforcing type definitions. You can also use Object.freeze(object) to prevent all property modifications, or Object.seal(object) to allow value changes but block new properties. Freezing individual properties requires Object.defineProperty(object, 'name', { writable: false }), which stops reassignment while leaving other properties mutable.
Advanced Class Features and Behavior in Modern JavaScript

Static initialization blocks let you run setup code when the class is first evaluated, before any instances exist. Write static { ... } inside the class body, and the block executes once during class definition. Use cases include configuring static fields from external data, registering the class in a global registry, or running validation checks on static configuration. Static blocks have access to private static fields, making them useful for encapsulated initialization logic that you don’t want to expose as a method.
Generator methods turn a class method into an iterator by prefixing it with *. Write *items() { yield this.#first; yield this.#second; } and you can iterate over class data using for...of or spread syntax. Async methods prefix the method name with async and return promises, enabling await inside the method body. You can combine both patterns with async *stream() to create asynchronous generators that yield values over time, useful for paginated API calls or streaming data sources.
Advanced patterns classes support:
Static initialization blocks. Run setup logic once when the class loads, with access to private static fields.
Generator methods. Implement iteration protocols by yielding values in sequence, making objects usable in for...of loops.
Async methods. Handle asynchronous workflows with async/await, returning promises instead of blocking execution.
Iterable protocol. Define [Symbol.iterator]() as a generator to make instances directly iterable without helper methods.
Comparing JavaScript Classes to Prototype-Based and Factory Patterns

Classes are syntactic sugar over JavaScript’s prototype system. Before ES6, developers used constructor functions like function User(name) { this.name = name; } and attached methods via User.prototype.greet = function() { ... }. Classes streamline this by grouping constructor and methods in one declaration, but the underlying mechanics stay identical. Instances still link to a prototype object, and method lookup follows the same chain.
Object.create offers another approach, letting you specify a prototype directly without writing a constructor. Call Object.create(prototypeObject) and you get a new object with [[Prototype]] set to prototypeObject, bypassing the new keyword entirely. This pattern suits scenarios where you want to clone or extend existing objects without defining a formal class. Mixins use Object.assign to copy methods from multiple source objects into a target, enabling composition instead of single-parent inheritance.
Composition often replaces deep inheritance hierarchies by combining small, focused objects. Instead of a five-level class tree, you might build a Player object from separate movable, damageable, and renderable mixins, each adding specific capabilities. This avoids the fragility and coupling that deep inheritance introduces, especially in large codebases where changing a base class ripples through every descendant.
| Pattern | Key Benefit | Ideal Use Case |
|---|---|---|
| ES6 Class | Clear syntax, shared methods via prototype, straightforward inheritance | Modeling entities with shared structure and behavior (users, products, UI components) |
| Constructor Function | Works in older environments, explicit prototype assignment | Legacy codebases or environments without ES6 support |
| Object.create | Direct prototype linking without constructors, flexible object cloning | Creating variations of existing objects or prototypal inheritance without classes |
| Mixins / Composition | Combines capabilities from multiple sources, avoids rigid inheritance trees | Adding cross-cutting features (logging, validation) or building objects from reusable modules |
Using JavaScript Classes in Real Projects and Frameworks

React class components use JavaScript classes to manage component state and lifecycle. A component extends React.Component and defines methods like render(), componentDidMount(), and setState(). The this.state object holds component data, and calling this.setState({ count: 10 }) triggers a re-render. Class components were the primary pattern before React Hooks, and plenty of production codebases still rely on them for complex stateful logic and lifecycle control.
Event-driven architectures often use classes to model pub/sub behavior. An EventEmitter class maintains a registry of listeners and provides on(event, callback) to subscribe and emit(event, data) to publish. Node.js includes a built-in EventEmitter class, and browser code frequently implements custom emitters for decoupling modules. Classes suit this pattern because instance state (the listener registry) persists across method calls, and methods like removeListener manipulate that state predictably.
Practical examples where classes appear in production:
React class components. Manage UI state, lifecycle methods, and event handlers in legacy or complex components.
EventEmitter-style classes. Coordinate asynchronous workflows by publishing and subscribing to named events.
Stateful service objects. Encapsulate API clients, database connections, or game engines that maintain internal state across operations.
Data models. Represent domain entities (User, Order, Invoice) with validation, computed properties, and serialization methods.
Troubleshooting, Debugging, and Common Pitfalls in JavaScript Classes

Forget to call super() in a subclass constructor and JavaScript throws a ReferenceError the moment you try to use this. The error message explicitly states “Must call super constructor in derived class before accessing ‘this'”, pointing to the line where this appears. The fix is straightforward. Move super(parentArgs) to the first line of the constructor, before any this.property = value assignments.
Call a static method on an instance instead of the class and you’ll get a TypeError because the method doesn’t exist on the instance or its prototype. Define static clean() and try instance.clean() and the error reads “instance.clean is not a function”. The solution is to call ClassName.clean() directly. Trying to call an instance method on the class itself fails because instance methods live on the prototype, not on the constructor.
Forget new when instantiating a class and you’ll throw a TypeError with the message “Class constructor ClassName cannot be invoked without ‘new'”. Unlike constructor functions, which can be called without new and create unintended global pollution, classes enforce the new requirement and fail fast. See this error? Add new before the class name: const obj = new ClassName().
Modifying ClassName.prototype directly after defining the class can introduce subtle bugs. Adding or changing methods on the prototype affects all existing and future instances, which might seem convenient but creates action-at-a-distance problems in large codebases. One module patches a method and another relies on the original behavior? The second module breaks. Better to define all methods inside the class body or use composition to extend behavior.
Common pitfalls:
Missing super() in subclass constructors. Triggers ReferenceError when accessing this before calling super().
Calling static methods on instances. Results in “not a function” TypeError because static methods aren’t on the prototype.
Overriding parent methods without super. Shadows parent methods completely unless you call super.methodName() to retain parent logic.
Modifying the prototype externally. Affects all instances and can introduce unintended side effects across modules.
Forgetting new during instantiation. Throws TypeError immediately because classes can’t be invoked as regular functions.
Final Words
We jumped straight into the mechanics: classes as blueprints tied to prototypes, constructors initializing fields, and instance methods living on the prototype.
We also covered class declarations vs expressions, inheritance with extends/super, getters and setters, plus advanced features like static blocks and async methods.
You got practical examples, comparisons with factory and mixin patterns, and troubleshooting tips for common runtime errors.
With this foundation, javascript classes should feel practical — pick patterns that fit your code and ship confidently.
FAQ
Q: Which is the best course to learn JavaScript?
A: The best course to learn JavaScript is a project-based one that covers ES6 and classes; try freeCodeCamp or MDN for free learning, or Jonas Schmedtmann’s Complete JavaScript Course for paid structure.
Q: What are the classes in JavaScript?
A: Classes in JavaScript are blueprints for objects that set up prototypes; constructors initialize properties, instance methods live on the prototype, and typeof Class returns “function”.
Q: What’s harder, C++ or JavaScript?
A: C++ is generally harder than JavaScript because it requires manual memory management, stricter types, and compilation; JavaScript’s dynamic runtime is easier to start with but can be tricky at scale.
Q: Are classes still used in JavaScript?
A: Classes are still used in JavaScript; ES6 class syntax is common for readable object-oriented code, while prototypes, factory functions, and composition remain useful alternatives.

