Classes (ES6) Get Certified
ES6 introduced the class keyword, which provides a
cleaner and more familiar syntax for creating objects and
working with inheritance. Under the hood it still uses the same
prototype system you learned about in the previous lesson.
Basic Class Syntax
A class is defined with the class keyword. The
constructor method runs automatically when you
create a new instance with new.
What this code does: Defines a simple class with a constructor and an instance method, then creates an instance and calls the method.
class Person {
constructor(name) {
this.name = name;
}
greet() {
return "Hello, " + this.name;
}
}
const michael = new Person("Michael");
console.log(michael.greet());
console.log(typeof Person); // "function"
class Person — creates a constructor function under the hood.constructor(name) { this.name = name; } — runs automatically when you use new.greet() { return "Hello, " + this.name; } — placed on Person.prototype; shared by all instances.}const michael = new Person("Michael") — without new this would throw.console.log(michael.greet())console.log(typeof Person) — “function”; classes are special constructor functions.Class Fields
You can declare fields directly in the class body. They are initialized on every new instance.
What this code does: Uses a class field to give every instance a default value without putting it inside the constructor.
class Counter {
count = 0;
increment() {
this.count++;
return this.count;
}
}
const c = new Counter();
console.log(c.increment());
console.log(c.increment());
console.log(c.count);
class Countercount = 0 — initialized on each new instance (ES2022). Equivalent to setting it in the constructor.increment() { this.count++; return this.count; }}const c = new Counter()console.log(c.increment()) — 1console.log(c.increment()) — 2console.log(c.count) — 2Getters and Setters
Getters and setters let you control how a property is read or written while still using normal property syntax.
What this code does: Defines a getter for the computed
area and a setter for diameter that
updates the underlying radius.
class Circle {
constructor(radius) {
this.radius = radius;
}
get area() {
return Math.PI * this.radius * this.radius;
}
set diameter(value) {
this.radius = value / 2;
}
}
const circle = new Circle(5);
console.log(circle.area.toFixed(2));
circle.diameter = 20;
console.log(circle.radius);
class Circleconstructor(radius) { this.radius = radius; }get area() { return Math.PI * this.radius * this.radius; } — accessed like a property (circle.area), not called as a method.set diameter(value) { this.radius = value / 2; } — assignment circle.diameter = 20 runs this body.}const circle = new Circle(5)console.log(circle.area.toFixed(2))circle.diameter = 20 — updates radius to 10.console.log(circle.radius)Static Methods
A static method belongs to the class itself, not
to individual instances. It is useful for utility functions
related to the class.
What this code does: Creates a static utility method that belongs to the class and can be called without creating an instance.
class MathHelper {
static square(n) {
return n * n;
}
}
console.log(MathHelper.square(4));
class MathHelperstatic square(n) { return n * n; } — belongs to the class, not to instances. Call as MathHelper.square(4).}console.log(MathHelper.square(4)) — 16. Calling it on an instance would fail or be undefined.Static Fields
Static fields belong to the class rather than to any single instance. They are commonly used to track information shared across all instances.
What this code does: Uses a static field to count how many times the class has been instantiated.
class Counter {
static instanceCount = 0;
constructor() {
Counter.instanceCount++;
}
}
new Counter();
new Counter();
new Counter();
console.log(Counter.instanceCount);
class Counterstatic instanceCount = 0 — one value shared by the whole class, not per instance.constructor() { Counter.instanceCount++; }}new Counter(); new Counter(); new Counter();console.log(Counter.instanceCount) — 3Private Fields
Prefixing a field name with # makes it private —
it can only be accessed from inside the class itself.
What this code does: Declares a private field #balance
that can only be read or modified from inside the class methods.
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance += amount;
return this.#balance;
}
get balance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
console.log(account.balance);
// account.#balance would cause a SyntaxError outside the class
class BankAccount#balance = 0 — the # prefix makes it truly private; inaccessible outside the class body.deposit(amount) { this.#balance += amount; return this.#balance; }get balance() { return this.#balance; } — outside code can read via account.balance but cannot assign to #balance directly.}const account = new BankAccount()account.deposit(100)console.log(account.balance)// account.#balance would cause a SyntaxError outside the classPrivate Methods
Methods can also be made private with the same
# prefix. They are useful for internal logic that
should not be part of the class’s public interface.
What this code does: Uses a private method #validate
that is only callable from other methods of the same class.
class Account {
#validate(amount) {
return amount > 0;
}
deposit(amount) {
if (!this.#validate(amount)) {
throw new Error("Invalid amount");
}
return "Deposited " + amount;
}
}
const account = new Account();
console.log(account.deposit(50));
class Account#validate(amount) { return amount > 0; } — only callable from inside the class.deposit(amount) {if (!this.#validate(amount)) { throw new Error("Invalid amount"); }return "Deposited " + amount} }const account = new Account()console.log(account.deposit(50))Class Expressions
Just like functions, classes can also be written as expressions and assigned to a variable.
What this code does: Shows that a class can be written as an expression and assigned to a variable.
const Greeter = class {
constructor(name) {
this.name = name;
}
greet() {
return "Hi, " + this.name;
}
};
const g = new Greeter("Michael");
console.log(g.greet());
const Greeter = class { — classes can be expressions, not only declarations. Useful for one-off or conditionally created classes.constructor(name) { this.name = name; }greet() { return "Hi, " + this.name; }}const g = new Greeter("Michael")console.log(g.greet())Inheritance with extends and super
A class can inherit from another class using
extends. Inside a subclass, super
refers to the parent class.
What this code does: Demonstrates inheritance with
extends and method overriding that calls the parent
version via super.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + " makes a sound.";
}
}
class Dog extends Animal {
speak() {
return super.speak() + " Specifically, a bark.";
}
}
const rex = new Dog("Rex");
console.log(rex.speak());
class Dog extends Animal — sets up the prototype chain automatically.speak() {return super.speak() + " Specifically, a bark." — calls the parent implementation, then extends it.} }const rex = new Dog("Rex")console.log(rex.speak())Instance vs. Static vs. Private Members
| Member Type | Accessed On | Visible Outside the Class? |
|---|---|---|
| Instance method / field | An instance (e.g. michael.greet()) | Yes |
| Static method / field | The class itself (e.g. MathHelper.square()) | Yes |
| Private field / method (#name) | Only from inside the class | No |
| Getter / setter | An instance, accessed like a property | Yes |
Common Mistakes with Classes
- Forgetting the
newkeyword when creating an instance. - Calling a static method on an instance instead of the class, or vice versa.
- Trying to access a private field or method (
#name) from outside the class. - Extracting a method from an instance and losing its
thisbinding. - Forgetting
super()before usingthisin a subclass constructor.
What this code does: Extracts a method from an instance and shows
that calling it without the original object causes a
this-related error.
class Person {
constructor(name) {
this.name = name;
}
greet() {
return "Hello, " + this.name;
}
}
const michael = new Person("Michael");
const greetFn = michael.greet;
try {
console.log(greetFn());
} catch (error) {
console.log("Error:", error.message);
}
const michael = new Person("Michael")const greetFn = michael.greet — detaches the function from its object; this is no longer bound.try {console.log(greetFn()) — in strict mode (classes are strict), this is undefined → TypeError.} catch (error) {console.log("Error:", error.message)} — fix with michael.greet.bind(michael) or an arrow wrapper.Best Practices for Classes
- Keep the
constructorfocused on setting up initial state. - Use private fields (
#name) to protect internal state. - Prefer getters for computed, read-only values.
- Use static methods for behavior related to the class as a whole.
- Keep inheritance chains shallow; prefer composition when appropriate.
Quick Summary
- The
constructormethod runs automatically when a class is instantiated withnew. - Instance methods are shared through the class’s prototype; class fields are declared directly in the class body.
- Getters and setters let computed properties be accessed and assigned like normal properties.
staticmembers belong to the class itself, not to individual instances.- Private fields and methods (
#name) are only accessible from inside the class. - Classes can also be written as expressions, not just declarations.
extendsandsuperprovide inheritance between classes.
Try It Yourself
Modify the class below. Try adding a new private field, a static counter, or a getter that computes something from existing fields.
What this code does: A complete practice class combining a static counter, a private price field, a getter for formatted output, and a static helper method.
class Product {
static totalProducts = 0;
#price;
constructor(name, price) {
this.name = name;
this.#price = price;
Product.totalProducts++;
}
get formattedPrice() {
return "$" + this.#price.toFixed(2);
}
static getTotalProducts() {
return Product.totalProducts;
}
}
const laptop = new Product("Laptop", 999.99);
const mouse = new Product("Mouse", 25);
console.log(laptop.name, laptop.formattedPrice);
console.log(mouse.name, mouse.formattedPrice);
console.log("Total products:", Product.getTotalProducts());
class Productstatic totalProducts = 0 — tracks how many products exist across all instances.#price — not readable or writable from outside.constructor(name, price) {this.name = namethis.#price = priceProduct.totalProducts++}get formattedPrice() { return "$" + this.#price.toFixed(2); }static getTotalProducts() { return Product.totalProducts; }}const laptop = new Product("Laptop", 999.99)const mouse = new Product("Mouse", 25)console.log(laptop.name, laptop.formattedPrice)console.log(mouse.name, mouse.formattedPrice)console.log("Total products:", Product.getTotalProducts()) — 2Tip: Use instance members for per-object data and behavior, static members for anything belonging to the class as a whole, and private fields whenever internal state should not be touched from outside the class.
Understanding ES6 classes gives you the modern, readable syntax for the same prototype-based system covered in the Prototypes & Inheritance lesson, and forms the foundation for object-oriented patterns in JavaScript.
🧪 Test Your JavaScript Code
Edit the code on the left and click “Run Code” to see the console output on the right.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.