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.

JavaScriptintermediate
13 min read

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:

javascriptjavascript
// 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:

javascriptjavascript
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:

javascriptjavascript
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); // true

Static Methods

Static methods belong to the class itself, not instances. They are called on the class name:

javascriptjavascript
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) → TypeError

Static methods are commonly used as factory methods — alternative constructors:

javascriptjavascript
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:

javascriptjavascript
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);     // 100

Class Fields

Class fields (public and private) can be declared at the class body level without using this in the constructor:

javascriptjavascript
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() {})
LocationOwn property (instance)ClassName.prototype
Shared across instancesNo (each gets own copy)Yes (shared via prototype)
Typical useInstance dataBehavior/actions

Strict Mode in Classes

Class bodies always run in strict mode, even without "use strict". This means:

  • this inside a method called without a receiver is undefined (not window)
  • Assigning to undeclared variables throws a ReferenceError
  • Duplicate parameter names are not allowed
javascriptjavascript
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:

javascriptjavascript
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

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
RunePowered by Rune AI

Frequently Asked Questions

Can a class have multiple constructors?

No. JavaScript classes allow exactly one `constructor` method per class. To handle multiple initialization patterns, use static factory methods or default parameter values.

Are class methods iterable?

Class instance methods are non-enumerable, so they do not show up in `for...in` loops or `Object.keys()`. This is different from manually adding methods to a plain object literal (which are enumerable by default).

What is the difference between a class field and a property set in the constructor?

Both create an own property on the instance. Class fields are syntactic shorthand and are initialized before the constructor body runs. They are especially useful for setting defaults and declaring private fields (`#field`).

Can I extend a class within a module and export it?

Yes. `export class Foo extends Bar {}` is valid. You can also `export default class {}` (anonymous class export).

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.