A class is a blueprint for objects that share the same shape and behaviour. You describe the data each object holds and the methods that work on it once, then create as many instances as you need. Classes also give JavaScript real private state, which plain objects cannot offer.
A first class
class Course {
constructor(title, lessons) {
this.title = title;
this.lessons = lessons;
}
describe() {
return `${this.title}: ${this.lessons} lessons`;
}
}
const js = new Course("JavaScript Basics", 12);
const css = new Course("CSS Grid in Practice", 8);
console.log(js.describe()); // → JavaScript Basics: 12 lessons
console.log(css.describe()); // → CSS Grid in Practice: 8 lessons
console.log(js instanceof Course); // → true
class Course { ... }declares the class. By convention, class names start with a capital letter.constructorruns when you callnew Course(...). Inside it,thisis the new object.describe()is a method. It is defined once and shared by every instance, rather than copied into each.newis required. CallingCourse("x", 1)without it throws aTypeError.
Class bodies always run in strict mode. Unlike function declarations, classes are not usable before their declaration: like let, they sit in the temporal dead zone until the line that declares them runs.
Class fields
Fields declare instance properties directly in the class body, with an optional starting value. They make the shape of an object visible at a glance:
class Course {
completed = 0;
tags = [];
constructor(title, lessons) {
this.title = title;
this.lessons = lessons;
}
}
const course = new Course("JavaScript Basics", 12);
console.log(course.completed); // → 0
Each instance gets its own copy of every field, so tags is a fresh array per course, not one shared array.
Private fields and methods
Anything on this can be read and changed from outside. That lets other code set course.completed = 500 and break your rules. A name starting with # creates a private field that only code inside the class body can access:
class Course {
#completed = 0;
constructor(title, lessons) {
this.title = title;
this.lessons = lessons;
}
completeLesson() {
if (this.#completed < this.lessons) {
this.#completed += 1;
}
return this.#progressText();
}
#progressText() {
return `${this.#completed} of ${this.lessons} lessons done`;
}
}
const course = new Course("Accessible Forms", 2);
console.log(course.completeLesson()); // → 1 of 2 lessons done
console.log(course.completeLesson()); // → 2 of 2 lessons done
console.log(course.completeLesson()); // → 2 of 2 lessons done
console.log(course.completed); // → undefined
Writing course.#completed outside the class is a SyntaxError, reported before any of the code runs. The # is part of the name, and the field does not show up in Object.keys(), JSON.stringify() or a for...in loop. This is true privacy, enforced by the language, unlike the old convention of an underscore prefix such as _completed, which was only a polite request.
Private fields must be declared in the class body before they are used. Private methods, like #progressText, work the same way. Both are supported in all current browsers and Node.js.
DevTools shows private fields when you inspect an object in the console. That is a debugging aid, not a leak: your code still cannot reach them.
Getters and setters
A getter runs a method when a property is read, so a value that is calculated from others looks like a normal property:
class Course {
#completed = 0;
constructor(title, lessons) {
this.title = title;
this.lessons = lessons;
}
get percent() {
return Math.round((this.#completed / this.lessons) * 100);
}
completeLesson() {
this.#completed = Math.min(this.#completed + 1, this.lessons);
}
}
const course = new Course("CSS Grid in Practice", 8);
course.completeLesson();
course.completeLesson();
console.log(course.percent); // → 25 (no brackets: it reads like a property)
Without a matching set percent(value), assigning to course.percent does nothing in sloppy code and throws a TypeError in strict code, including modules. Lesson 46 covers getters, setters and validation in depth.
Static members
A static method or field belongs to the class itself, not to instances. Static methods suit helpers that create or compare instances:
class Course {
static levels = ["Beginner", "Intermediate", "Advanced"];
constructor({ id, title, level, lessons }) {
if (!Course.levels.includes(level)) {
throw new RangeError(`Unknown level: ${level}`);
}
this.id = id;
this.title = title;
this.level = level;
this.lessons = lessons;
}
static fromJSON(list) {
return list.map((data) => new Course(data));
}
static byLessons(a, b) {
return a.lessons - b.lessons;
}
}
const courses = Course.fromJSON([
{ id: 1, title: "JavaScript Basics", level: "Beginner", lessons: 12 },
{ id: 2, title: "CSS Grid in Practice", level: "Intermediate", lessons: 8 },
]);
courses.sort(Course.byLessons);
console.log(courses.map((c) => c.title)); // → [ 'CSS Grid in Practice', 'JavaScript Basics' ]
console.log(Course.levels.length); // → 3
Course.fromJSON turns the plain objects from the dashboard's courses.json into Course instances. You call static members on the class (Course.fromJSON), never on an instance.
Inheritance with extends
extends creates a class that starts with everything another class has, then adds or changes behaviour:
class Course {
constructor(title, lessons) {
this.title = title;
this.lessons = lessons;
}
describe() {
return `${this.title}: ${this.lessons} lessons`;
}
}
class VideoCourse extends Course {
constructor(title, lessons, minutes) {
super(title, lessons); // must come before using this
this.minutes = minutes;
}
describe() {
return `${super.describe()}, ${this.minutes} minutes of video`;
}
}
const video = new VideoCourse("Flexbox Fundamentals", 6, 95);
console.log(video.describe());
// → Flexbox Fundamentals: 6 lessons, 95 minutes of video
console.log(video instanceof VideoCourse); // → true
console.log(video instanceof Course); // → true
- In a subclass constructor, call
super(...)before you touchthis. Usingthisfirst throws aReferenceError. - If a subclass has no constructor, it gets one that passes all arguments to
super. super.describe()calls the parent's version of a method you have overridden.- Private fields are private to the class that declares them. A subclass cannot read its parent's
#completed; it must use the parent's public methods.
You already used extends in lesson 35 to create custom errors. That is one of its best uses.
Keep inheritance shallow. One level, such as VideoCourse extends Course or HttpError extends Error, is easy to follow. Deep chains of classes that each change a little are hard to reason about. When you want to share behaviour between unrelated classes, pass helper objects or functions in instead.
Methods and this
Methods are ordinary functions, so the rules from lesson 21 apply. Passing a method as a callback loses this:
class Counter {
count = 0;
increment() {
this.count += 1;
}
}
const counter = new Counter();
const button = document.querySelector("#add");
button.addEventListener("click", counter.increment); // this is the button, not counter
Two fixes are common: wrap the call in an arrow function, or define the method as an arrow-function field, which captures the instance:
button.addEventListener("click", () => counter.increment());
class SafeCounter {
count = 0;
increment = () => {
this.count += 1;
};
}
The arrow-field version creates a new function per instance, which costs a little memory, so use it only for methods you pass around as callbacks.
Classes are functions underneath
console.log(typeof Course); // → function
A class is a special kind of function, and its methods live on Course.prototype, shared by all instances. Lesson 41 opens up that machinery and shows what class and extends do behind the scenes.
When to use a class
Use a class when you have many objects with the same shape and behaviour, when you need private state with rules around it, or when a library or platform API expects one (custom elements and custom errors both do). For a single object, or for data that is just passed around and displayed, a plain object and a few functions are simpler. The dashboard's course data, for example, works fine as plain objects until it needs behaviour of its own.
Summary
classdeclares a blueprint;constructorsets up each instance created withnew.- Methods are shared by all instances; class fields create per-instance properties.
#namefields and methods are truly private to the class body.getdefines a calculated property;staticmembers belong to the class itself.extendsbuilds a subclass; callsuper()before usingthis, andsuper.method()to reuse the parent's version.- Methods lose
thiswhen passed as callbacks; wrap them in an arrow function or use an arrow-function field. - Classes are functions with prototypes underneath, which lesson 41 explains.