JavaScript Classes Explained: Complete Tutorial
A complete guide to JavaScript ES6 classes. Covers class declarations and expressions, constructors, instance methods, static methods, getters/setters, class fields, hoisting behavior, and how classes map to the underlying prototype system.
ES6 classes, introduced in 2015, give JavaScript a clean object-oriented syntax. Classes are not a new inheritance model — they are syntactic sugar over JavaScript's existing prototype-based system. Everything a class does could be written with constructor functions and prototype assignments, but class syntax is more readable, adds useful guardrails, and is now the preferred pattern for object-oriented JavaScript.
Class Declaration vs Class Expression
Classes can be declared or used as expressions:
// Class declaration
class User {
constructor(name) {
this.name = name;
}
}
// Class expression (anonymous)
const Product = class {
constructor(title, price) {
this.title = title;
this.price = price;
}
};
// Class expression (named — name only accessible inside class body)
const Animal = class AnimalClass {
constructor(species) { this.species = species; }
describe() { return AnimalClass.name; } // "AnimalClass" accessible here
};
const u = new User("Alice");
const p = new Product("Laptop", 999);
const a = new Animal("Dog");Hoisting difference: Function declarations are hoisted; class declarations are NOT. Accessing a class before its declaration throws a ReferenceError (temporal dead zone, same as let/const).
The constructor Method
Each class can have exactly one constructor method. It runs when new ClassName() is called:
class Rectangle {
constructor(width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Dimensions must be positive");
}
this.width = width;
this.height = height;
}
}
const r = new Rectangle(5, 3);If you omit the constructor, JavaScript adds an implicit empty one: constructor() {}. For derived classes (using extends), the implicit constructor calls super() with all arguments.
Instance Methods
Methods defined in the class body become non-enumerable properties on ClassName.prototype:
class Circle {
constructor(radius) {
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
circumference() {
return 2 * Math.PI * this.radius;
}
toString() {
return `Circle(r=${this.radius})`;
}
}
const c = new Circle(5);
console.log(c.area().toFixed(2)); // "78.54"
console.log(c.circumference().toFixed(2)); // "31.42"
console.log(String(c)); // "Circle(r=5)"
// Methods are on the prototype, not the instance
console.log(c.hasOwnProperty("area")); // false
console.log("area" in Circle.prototype); // trueStatic Methods
Static methods belong to the class itself, not instances. They are called on the class name:
class MathUtils {
static square(x) { return x * x; }
static cube(x) { return x * x * x; }
static clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
}
console.log(MathUtils.square(4)); // 16
console.log(MathUtils.cube(3)); // 27
console.log(MathUtils.clamp(15, 0, 10)); // 10
// Static methods are NOT on instances
const m = new MathUtils();
// m.square(4) → TypeErrorStatic methods are commonly used as factory methods — alternative constructors:
class Color {
constructor(r, g, b) {
this.r = r; this.g = g; this.b = b;
}
// Factory static method
static fromHex(hex) {
const n = parseInt(hex.replace("#", ""), 16);
return new Color((n >> 16) & 255, (n >> 8) & 255, n & 255);
}
toHex() {
return `#${[this.r, this.g, this.b]
.map(c => c.toString(16).padStart(2, "0"))
.join("")}`;
}
}
const red = new Color(255, 0, 0);
const green = Color.fromHex("#00ff00");
console.log(red.toHex()); // "#ff0000"
console.log(green.toHex()); // "#00ff00"For a deep dive on static, see JavaScript static methods tutorial.
Getters and Setters
Getters and setters define computed or controlled properties using get/set keywords:
class Temperature {
constructor(celsius) {
this._celsius = celsius; // Stored as celsius internally
}
// Getter — accessed as a property, not called as a method
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
// Setter — validates and converts before storing
set fahrenheit(f) {
this._celsius = (f - 32) * 5/9;
}
get celsius() { return this._celsius; }
set celsius(c) {
if (c < -273.15) throw new RangeError("Below absolute zero");
this._celsius = c;
}
}
const t = new Temperature(0);
console.log(t.fahrenheit); // 32
t.fahrenheit = 212;
console.log(t.celsius); // 100Class Fields
Class fields (public and private) can be declared at the class body level without using this in the constructor:
class Counter {
// Public class field — initialized per instance
count = 0;
step = 1;
constructor(initialCount = 0) {
this.count = initialCount;
}
increment() { this.count += this.step; }
decrement() { this.count -= this.step; }
reset() { this.count = 0; }
}
// Static class field
class Config {
static version = "1.0.0";
static defaultTimeout = 3000;
}
console.log(Config.version); // "1.0.0"Class fields are instance properties, not prototype properties — each instance gets its own copy. This distinguishes them from prototype methods:
Class Field (x = value) | Method (method() {}) | |
|---|---|---|
| Location | Own property (instance) | ClassName.prototype |
| Shared across instances | No (each gets own copy) | Yes (shared via prototype) |
| Typical use | Instance data | Behavior/actions |
Strict Mode in Classes
Class bodies always run in strict mode, even without "use strict". This means:
thisinside a method called without a receiver isundefined(notwindow)- Assigning to undeclared variables throws a
ReferenceError - Duplicate parameter names are not allowed
class Strict {
test() {
// this = undefined when called standalone
return typeof this;
}
}
const t = new Strict();
const fn = t.test;
// fn() → returns "undefined" (strict mode), not "object" (sloppy mode)Prototype Mapping
Understanding what class syntax generates helps debug class-related issues:
class Dog {
constructor(name) { this.name = name; }
bark() { return `${this.name} barks!`; }
static create(name) { return new Dog(name); }
}
// Class generates this structure:
// Dog is a function
console.log(typeof Dog); // "function"
// Instance methods land on Dog.prototype
console.log(typeof Dog.prototype.bark); // "function"
// Static methods land on Dog itself
console.log(typeof Dog.create); // "function"
// Prototype is non-enumerable
console.log(Object.keys(Dog.prototype)); // [] (non-enumerable methods)This contrasts with modifying the prototype directly where you must manage enumerability manually.
Rune AI
Key Insights
- Classes are not hoisted: Unlike function declarations, class declarations are in the temporal dead zone — accessing before declaration throws a ReferenceError
- Class bodies are strict mode: this in a detached method is undefined, not the global object; undeclared variables throw errors
- Methods go on the prototype: Instance methods defined in the class body land on ClassName.prototype (non-enumerable) — they are shared, not per-instance
- Static methods belong to the class: Called as ClassName.method(), not available on instances; useful for factory methods and utility functions
- Class fields are own properties: Declared fields (like count = 0) are created directly on the instance object, not on the prototype
Frequently Asked Questions
Can a class have multiple constructors?
Are class methods iterable?
What is the difference between a class field and a property set in the constructor?
Can I extend a class within a module and export it?
Conclusion
JavaScript classes provide clean syntax for creating objects with shared behavior. The constructor method initializes instances; prototype methods are shared across all instances; static methods belong to the class itself; getters/setters control property access; and class fields declare per-instance state. Internally, classes are powered by the same prototype system as constructor functions — the syntax is just cleaner and more explicit. With inheritance via extends and super, classes become even more powerful, covered fully in JavaScript class inheritance.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.