JavaScript classes are a way to create objects and organize code in a clean, structured way. Introduced in ES6, classes provide a clear and straightforward syntax to build objects, hiding the prototype-based inheritance model underneath. Although JavaScript still uses prototypes internally, classes give programmers a more familiar, classical style for object-oriented programming. This makes it easier to define blueprints for objects, create multiple instances, and share behaviors.
At its heart, a class acts like a template. You define properties and methods in the class, then create many objects (instances) from it, each with its own unique data but shared behavior. Classes also allow you to create hierarchies by extending other classes, letting new classes inherit features from parent ones. This article will guide you through creating classes step-by-step with fun, practical examples.
Defining a Basic Class
Creating a class starts with the class
keyword followed by the class name. Inside the class, you define a special method called constructor
where you set up initial properties for each instance. Let’s create a simple Wizard
class with a name and favorite spell.
class Wizard {
constructor(name, spell) {
this.name = name;
this.spell = spell;
}
}
const merlin = new Wizard("Merlin", "Fireball");
console.log(merlin.name); // Output: Merlin
console.log(merlin.spell); // Output: Fireball
In this example, the Wizard
class has a constructor that assigns the name
and spell
properties. We create a new wizard named Merlin by calling new Wizard(...)
. The new
keyword makes a new object using the class blueprint. Merlin’s name and spell are stored in the object’s properties.
Adding Methods to a Class
Classes are not just about properties. They can have methods — functions that belong to instances of the class. Inside the class, simply write a method without the function
keyword. Here, we add a method castSpell
that announces the wizard casting their spell.
class Wizard {
constructor(name, spell) {
this.name = name;
this.spell = spell;
}
castSpell() {
console.log(`${this.name} casts ${this.spell}!`);
}
}
const morgana = new Wizard("Morgana", "Lightning Bolt");
morgana.castSpell(); // Output: Morgana casts Lightning Bolt!
The castSpell
method uses the this
keyword to refer to the current object. When we call morgana.castSpell()
, it prints a message showing which spell Morgana casts.
Class Inheritance with extends
Classes can inherit from other classes, gaining their properties and methods. This lets you build more specific classes from general ones. We use the extends
keyword to create a subclass. For example, we make FireWizard
inherit from Wizard
and add a new method fireBlast
.
class Wizard {
constructor(name, spell) {
this.name = name;
this.spell = spell;
}
castSpell() {
console.log(`${this.name} casts ${this.spell}!`);
}
}
class FireWizard extends Wizard {
fireBlast() {
console.log(`${this.name} unleashes a mighty fire blast!`);
}
}
const pyro = new FireWizard("Pyro", "Flame Thrower");
pyro.castSpell(); // Output: Pyro casts Flame Thrower!
pyro.fireBlast(); // Output: Pyro unleashes a mighty fire blast!
FireWizard
inherits the constructor and methods from Wizard
. It can call castSpell()
even though it’s not defined in FireWizard
. Plus, it adds a unique fireBlast()
method. This shows how inheritance extends capabilities.
Using super
to Call Parent Class Constructor and Methods
When a subclass defines its own constructor, it must call the parent class constructor using super()
. This ensures the inherited properties are set correctly. We’ll add a constructor to FireWizard
to also include a powerLevel
.
class Wizard {
constructor(name, spell) {
this.name = name;
this.spell = spell;
}
castSpell() {
console.log(`${this.name} casts ${this.spell}!`);
}
}
class FireWizard extends Wizard {
constructor(name, spell, powerLevel) {
super(name, spell); // Calls Wizard constructor
this.powerLevel = powerLevel;
}
fireBlast() {
console.log(`${this.name} unleashes a fire blast with power ${this.powerLevel}!`);
}
}
const blaze = new FireWizard("Blaze", "Inferno", 9000);
blaze.castSpell(); // Output: Blaze casts Inferno!
blaze.fireBlast(); // Output: Blaze unleashes a fire blast with power 9000!
Here, super(name, spell)
invokes the Wizard
constructor, setting name
and spell
. Then powerLevel
is set in the subclass. The fireBlast
method can now use this extra information.
Getters and Setters in Classes
Classes can have getters and setters for properties that behave like normal fields but run code when accessed or changed. This allows computed or controlled properties. Let’s add a getter and setter for a wizard’s level
.
class Wizard {
constructor(name, spell, level = 1) {
this.name = name;
this.spell = spell;
this._level = level; // Convention: underscore for "private" property
}
get level() {
return this._level;
}
set level(value) {
if (value > 0) {
this._level = value;
} else {
console.log("Level must be positive.");
}
}
}
const gandalf = new Wizard("Gandalf", "Light Beam", 5);
console.log(gandalf.level); // Output: 5
gandalf.level = 10;
console.log(gandalf.level); // Output: 10
gandalf.level = -3; // Output: Level must be positive.
The getter level()
returns the private _level
property. The setter validates the input before changing _level
. Using getters and setters hides internal data control from users of the class.
Static Methods and Properties
Static methods belong to the class itself, not to instances. They are often used for utility functions related to the class. Define them using the static
keyword. For example, let’s add a static method to create a random wizard.
class Wizard {
constructor(name, spell) {
this.name = name;
this.spell = spell;
}
castSpell() {
console.log(`${this.name} casts ${this.spell}!`);
}
static createRandomWizard() {
const names = ["Albus", "Merlin", "Morgana", "Gandalf"];
const spells = ["Fireball", "Ice Shard", "Lightning Bolt", "Heal"];
const name = names[Math.floor(Math.random() * names.length)];
const spell = spells[Math.floor(Math.random() * spells.length)];
return new Wizard(name, spell);
}
}
const randomWizard = Wizard.createRandomWizard();
randomWizard.castSpell();
The method createRandomWizard
returns a new wizard with random attributes. Notice we call it directly on the class, not on an instance.
Private Fields and Methods
Modern JavaScript allows private fields and methods using the #
prefix. These members cannot be accessed from outside the class, enforcing encapsulation.
class Wizard {
#mana = 100;
constructor(name, spell) {
this.name = name;
this.spell = spell;
}
showMana() {
console.log(`${this.name} has ${this.#mana} mana.`);
}
#useMana(amount) {
if (this.#mana >= amount) {
this.#mana -= amount;
console.log(`${this.name} uses ${amount} mana.`);
} else {
console.log(`${this.name} doesn't have enough mana.`);
}
}
castSpell() {
this.#useMana(20);
console.log(`${this.name} casts ${this.spell}!`);
}
}
const sorcerer = new Wizard("Sorcerer", "Magic Missile");
sorcerer.showMana(); // Output: Sorcerer has 100 mana.
sorcerer.castSpell(); // Output: Sorcerer uses 20 mana.
// Sorcerer casts Magic Missile!
sorcerer.showMana(); // Output: Sorcerer has 80 mana.
Here, #mana
and #useMana
are private and cannot be accessed directly. Methods inside the class manage these private members.
Fun Example: A Magical Duel Between Wizards
To see classes in action, let’s create two wizard classes that battle using spells and health points. We’ll use inheritance and methods for a simple duel.
class Wizard {
constructor(name, health = 100) {
this.name = name;
this.health = health;
}
castSpell(opponent) {
const damage = Math.floor(Math.random() * 20) + 1;
opponent.health -= damage;
console.log(`${this.name} casts a spell causing ${damage} damage to ${opponent.name}!`);
}
isAlive() {
return this.health > 0;
}
}
class FireWizard extends Wizard {
castFireBlast(opponent) {
const damage = Math.floor(Math.random() * 30) + 10;
opponent.health -= damage;
console.log(`${this.name} casts Fire Blast causing ${damage} damage to ${opponent.name}!`);
}
}
const harry = new Wizard("Harry");
const sirius = new FireWizard("Sirius");
while (harry.isAlive() && sirius.isAlive()) {
harry.castSpell(sirius);
if (!sirius.isAlive()) break;
sirius.castFireBlast(harry);
}
console.log(`${harry.name} has ${harry.health} health left.`);
console.log(`${sirius.name} has ${sirius.health} health left.`);
In this duel, each wizard attacks the other until one falls. The randomness adds excitement, showing how class methods manage state and interact.
Conclusion
JavaScript classes provide a clean, organized way to define objects and their behaviors. Through constructors, methods, inheritance with extends
, and features like getters, setters, and static methods, classes simplify object-oriented programming. Private fields add encapsulation, keeping internal details hidden. With these tools, you can model real-world concepts like wizards and magical duels in elegant, understandable code.
References
If you want to learn more about JavaScript classes and related concepts, these resources are excellent starting points:
- MDN Web Docs: Classes
The official documentation explaining class syntax and usage. - MDN Web Docs: Inheritance and the prototype chain
Learn how classes work with JavaScript’s prototype system. - MDN Web Docs: Static methods
Understand static methods and properties in classes. - MDN Web Docs: Private class fields
Explore the syntax and use cases for private fields and methods.