Tutorials

An Introduction to ES6 Classes

Key Takeaways

  • JavaScript classes provide constructor and inheritance syntax built on prototypes; they don't replace JavaScript's object model.
  • Constructor assignments and public instance fields create per-instance properties, while public instance methods are shared through the class's prototype.
  • A derived constructor must call super() before accessing this.
  • Public class fields and private elements were standardized in ES2022, not ES6 or ES2026.

JavaScript classes let you group instance data, initialization, and behavior using familiar object-oriented syntax. Underneath, a class is a special constructor function, and its public instance methods use JavaScript's prototype-based inheritance.

This introduction covers constructors, properties, methods, inheritance, and modern private elements. If the surrounding syntax is unfamiliar, start with our JavaScript ES6 introduction.

JavaScript ES6 Classes: A First Example

Start with an animal that has a name, a species, and one behavior:

class Animal {
  constructor(name, species) {
    this.name = name;
    this.species = species;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

const dog = new Animal('Charlie', 'Dog');
dog.speak(); // Charlie makes a noise.

The new Animal(...) expression creates an instance and runs its constructor. Inside that constructor, this refers to the new instance, so the assignments create its name and species properties.

The examples below can run in order in one JavaScript file. Blocks marked as fragments continue an earlier example rather than redefining its class.

How Classes Work With Prototypes

A prototype is another object that JavaScript consults when a property isn't found directly on an object. In this example, dog owns its data, but speak lives on Animal.prototype.

dog
  own properties: name, species
  |
  v
Animal.prototype
  shared method: speak
  |
  v
Object.prototype
  |
  v
null

You can inspect that relationship. Object.getPrototypeOf() returns an object's prototype, and Object.hasOwn() checks whether a property belongs directly to that object.

// Fragment: continues the Animal example.
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true
console.log(Object.hasOwn(dog, 'name')); // true
console.log(Object.hasOwn(dog, 'speak')); // false
console.log(dog.speak === Animal.prototype.speak); // true

Public methods declared in a class are also non-enumerable: they don't appear in for...in enumeration. Class syntax makes common constructor-and-prototype patterns easier to express, but the prototype model remains important.

JavaScript Class Constructor

The constructor initializes an instance from its arguments. Here's one Person declaration containing both of the methods we'll use later:

class Person {
  constructor(first, last) {
    this.first = first;
    this.last = last;
  }

  greet() {
    return `Hello, ${this.getFullName()}!`;
  }

  getFullName() {
    return `${this.first} ${this.last}`;
  }
}

const user = new Person('John', 'Doe');
console.log(user.greet()); // Hello, John Doe!

Use constructors for required inputs and initialization logic. If you omit a constructor, JavaScript supplies a default: an empty constructor for a base class, or one that forwards arguments to the parent constructor for a derived class.

Class Declaration Rules

  • Use new. Calling a class as an ordinary function throws a TypeError.
  • Declare it before using it. Unlike a function declaration, a class declaration cannot be used before execution reaches it. Its binding is in the temporal dead zone—the period before initialization when access throws a ReferenceError.
  • Expect strict mode. Class bodies run in strict mode even without a "use strict" directive.

ES6 Class Methods

Methods define behavior using the instance's data. Because getFullName() is already part of Person, you can create another person without declaring the class again:

// Fragment: continues the Person example.
const person = new Person('Alice', 'Smith');
console.log(person.getFullName()); // Alice Smith
console.log(person.getFullName === user.getFullName); // true

The two instances share the same method function through Person.prototype, but calling it through each instance supplies a different this. You don't need to copy a method onto every object to make it operate on different data.

Class Properties vs. Public Class Fields

this.first = first is an assignment executed by the constructor. A declaration such as first = '' inside the class body is a public class field—a later feature with its own initialization rules. Both can provide ordinary public instance properties.

class Member {
  first = '';
  last = '';
  nickname;
  tags = [];

  constructor(first, last) {
    this.first = first;
    this.last = last;
  }
}

const memberA = new Member('Ada', 'Lovelace');
const memberB = new Member('Grace', 'Hopper');

console.log(memberA.first); // Ada
console.log(memberA.nickname); // undefined
console.log(memberA.tags === memberB.tags); // false

Here, first and last begin as empty strings before the constructor overwrites them. A field without an initializer starts as undefined, and the [] initializer creates a separate array for each instance.

The important initialization differences are:

  • Base-class instance fields initialize before the constructor body runs.
  • Derived-class instance fields initialize after the parent constructor runs, as the super() call completes.
  • A public field declaration defines an own data property; unlike assignment, it doesn't invoke an inherited setter with the same name.

Use fields to make instance state and defaults visible at the top of the class. Use constructor assignments for values that depend on constructor arguments. Our guide to JavaScript objects provides more background on properties.

Where Class Members Live

DefinitionLocationTypical access
Constructor assignment: this.first = firstInstance, in this exampleperson.first
Public instance field: first = ''InstancememberA.first
Public instance method: getFullName() {}Class prototypeperson.getFullName()
Static field or methodClass constructor itselfPoint.dimensions or Point.origin()

This distinction separates per-object state from shared behavior and class-level utilities.

JavaScript Class Inheritance With extends and super()

Use extends to derive a class from another constructor. The subclass can inherit public methods, add behavior, and override methods with its own implementations.

// Fragment: run after the Person declaration above.
class FormalPerson extends Person {
  constructor(first, last, title) {
    super(first, last);
    this.title = title;
  }

  getFormalName() {
    return `${this.title} ${this.first} ${this.last}`;
  }

  greet() {
    return `${super.greet()} (${this.title})`;
  }
}

const formalPerson = new FormalPerson('Alan', 'Turing', 'Dr.');
console.log(formalPerson.getFormalName()); // Dr. Alan Turing
console.log(formalPerson.getFullName()); // Alan Turing
console.log(formalPerson.greet()); // Hello, Alan Turing! (Dr.)

super(first, last) invokes the parent constructor, which initializes first and last on the instance. Accessing this before that call in a derived constructor throws a ReferenceError. Separately, super.greet() calls the parent method while retaining the current instance as this.

Choose inheritance when the child genuinely fits the parent's contract. For unrelated capabilities, prefer composition: give an object a collaborator, such as a formatter or logger, rather than making it inherit from one. Sharing a few methods isn't, by itself, a good reason to create a class hierarchy.

Static Methods and Static Class Fields

Static members belong to the class constructor, not its instances. Static methods are useful for factory functions and utilities associated with a class; static fields hold class-level data.

class Point {
  static dimensions = 2;

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static origin() {
    return new Point(0, 0);
  }
}

const origin = Point.origin();
console.log(Point.dimensions); // 2
console.log(origin.x); // 0
console.log(origin.dimensions); // undefined

Static field initializers run when the class is evaluated, rather than once per instance. Remember that the method syntax is part of ES6, while the static dimensions = 2 field syntax is part of ES2022.

JavaScript Private Fields and Private Methods

A private field stores internal state; a private method implements internal behavior. Declare both using a # name. Private fields must be declared in the class body before you assign to them.

class SecureDocument {
  #content;
  static #processed = 0;

  constructor(content) {
    this.#content = content;
  }

  #validate() {
    return typeof this.#content === 'string'
      && this.#content.length > 0;
  }

  process() {
    if (!this.#validate()) {
      throw new Error('Document must contain text');
    }
    SecureDocument.#processed += 1;
    console.log('Document is valid');
  }

  static processedCount() {
    return SecureDocument.#processed;
  }
}

const doc = new SecureDocument('Meeting notes');
doc.process(); // Document is valid
console.log(SecureDocument.processedCount()); // 1

// doc.#validate(); // SyntaxError if uncommented outside the class.

JavaScript enforces this privacy. Private elements aren't ordinary string-keyed or symbol-keyed properties, so normal property lookup and reflection don't expose them. For example, doc['#content'] doesn't read the private field.

Private names are lexically scoped: their meaning comes from the class declaration containing the code. A subclass cannot directly name its parent's private elements; inherited public methods can still operate on them.

Uncommenting doc.#validate() in an ordinary JavaScript file causes a parse-time SyntaxError, not a failed runtime permission check. Some developer consoles relax this restriction for debugging, so use a script file to check the language rule.

Private Static Elements

The example's static #processed belongs to SecureDocument, not to each document. Private static methods use the same combination, such as static #helper() {}. Access private static elements through their declaring class name; using this can fail when an inherited static method is called on a subclass.

Getters and Setters

A getter runs when you read a property, and a setter runs when you assign to it. They let you keep property-style access while validating writes or computing values.

class Nickname {
  #value;

  constructor(value) {
    this.value = value;
  }

  get value() {
    return this.#value;
  }

  set value(value) {
    if (typeof value !== 'string') {
      throw new TypeError('Nickname must be a string');
    }
    this.#value = value;
  }
}

const nickname = new Nickname('Charlie');
nickname.value = 'Chuck';
console.log(nickname.value); // Chuck

Read nickname.value, not nickname.value(). The setter writes to a separate backing field, #value; assigning to this.value inside that setter would call the setter again.

Common Mistakes

  • Declaring the same class twice in one scope. Two class Person declarations in the same scope are a syntax error. Extend the existing example or give independent examples different names.
  • Losing the method receiver. With const greet = user.greet; greet();, the method's this is undefined. Use user.greet(), a wrapper such as () => user.greet(), or user.greet.bind(user) when passing a callback.
  • Treating an underscore as privacy. A name such as _content is an ordinary public property, not a language-enforced private element.

If callback behavior is surprising, review JavaScript functions before adding more class machinery.

FAQ

Are JavaScript classes just syntactic sugar over prototypes?

Classes build on prototypes: public instance methods live on the class prototype and are shared by instances. But classes also enforce specific rules, including strict-mode bodies, construction with new, and declaration initialization restrictions, so they aren't interchangeable with every constructor-function pattern.

What is the difference between class fields and constructor properties?

A public instance field is declared in the class body and initialized automatically for each instance. Constructor assignment runs when execution reaches that statement; unlike a field declaration, it can invoke an inherited setter.

How do you declare a private field in JavaScript?

Declare a hash-prefixed name in the class body, such as #value = 0, and access it as this.#value from code in the class. Private fields store state and were standardized in ES2022, not ES6.

How do you create a private method in JavaScript?

Declare a hash-prefixed method, such as #validate() {}, inside the class and call it there using this.#validate(). Private methods were standardized in ES2022 and don't require TypeScript; referencing their private names outside the declaring class's private scope is a syntax error.

When must a derived class call super()?

A derived constructor must call super() before accessing this. If you don't declare a constructor in the subclass, JavaScript supplies one that forwards its arguments to the parent constructor.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me