24. Classes
A JavaScript class is syntactic sugar over prototype-based inheritance. Classes make it easy to create object blueprints with an approach more similar to traditional programming languages like Java or C++.
Why Do We Need Classes?
Before ES6, we had to use constructor functions, which were less intuitive. Classes bring cleaner, easier-to-understand syntax.
// Old way — constructor function
function Student(name, studentId) {
this.name = name;
this.studentId = studentId;
}
Student.prototype.getInfo = function() {
return `${this.name} (${this.studentId})`;
};
// New way — class
class Student {
constructor(name, studentId) {
this.name = name;
this.studentId = studentId;
}
getInfo() {
return `${this.name} (${this.studentId})`;
}
}
Code Example
Class Declaration
class Student {
constructor(name, studentId, major) {
this.name = name;
this.studentId = studentId;
this.major = major;
this.courseList = [];
}
getInfo() {
return `${this.name} | StudentID: ${this.studentId} | ${this.major}`;
}
addCourse(courseName) {
this.courseList.push(courseName);
return `${courseName} added to schedule ${this.name}`;
}
getCourseCount() {
return this.courseList.length;
}
}
const mike = new Student("Mike Johnson", "2024001", "Computer Science");
console.log(mike.getInfo());
// "Mike Johnson | StudentID: 2024001 | Computer Science"
console.log(mike.addCourse("Algorithms & Data Structures"));
// "Algorithms & Data Structures added to schedule Mike Johnson"
console.log(mike.getCourseCount()); // 1
Constructor
class Lecturer {
constructor(name, nidn, course) {
this.name = name;
this.nidn = nidn;
this.course = course || []; // default is empty
this.active = true;
}
getInfo() {
const status = this.active ? "Active" : "Inactive";
return `Dr. ${this.name} (${this.nidn}) - ${status}`;
}
}
const lecturer = new Lecturer("John Smith", "0012345678", ["Database"]);
console.log(lecturer.getInfo());
// "Dr. John Smith (0012345678) - Active"
Getters and Setters
class Course {
#credits; // private field
constructor(name, credits, semester) {
this.name = name;
this.#credits = credits;
this.semester = semester;
this._grade = 0; // convention: underscore = "internal use"
}
// Getter — accessed like a property, not a method
get info() {
return `${this.name} (${this.#credits} credits) - Semester ${this.semester}`;
}
get grade() {
return this._grade;
}
// Setter — set the value with validation
set grade(grade) {
if (grade < 0 || grade > 100) {
console.error("Grade must be between 0 - 100!");
return;
}
this._grade = grade;
}
getGrade() {
if (this._grade >= 85) return "A";
if (this._grade >= 70) return "B";
if (this._grade >= 55) return "C";
if (this._grade >= 40) return "D";
return "E";
}
}
const course = new Course("Web Programming", 3, 3);
console.log(course.info); // "Web Programming (3 credits) - Semester 3"
course.grade = 90; // setter
console.log(course.grade); // 90
console.log(course.getGrade()); // "A"
course.grade = 105; // error: "Grade must be between 0 - 100!"
Static Methods
class AcademicSystem {
static #instance = null;
static students = [];
// Singleton pattern
static getInstance() {
if (!AcademicSystem.#instance) {
AcademicSystem.#instance = new AcademicSystem();
}
return AcademicSystem.#instance;
}
static addStudent(student) {
AcademicSystem.students.push(student);
return true;
}
static findByStudentID(studentId) {
return AcademicSystem.students.find(m => m.studentId === studentId);
}
static getStudentCount() {
return AcademicSystem.students.length;
}
// Factory method
static createStudent(name, studentId, major) {
const new = new Student(name, studentId, major);
AcademicSystem.addStudent(new);
return new;
}
}
// Static methods — called directly from the class, not an instance
const student1 = AcademicSystem.createStudent("Mike", "2024001", "CS");
const student2 = AcademicSystem.createStudent("Anna", "2024002", "IS");
console.log(AcademicSystem.getStudentCount()); // 2
console.log(AcademicSystem.findByStudentID("2024001").getInfo());
Private Fields with #
class CampusAccount {
#accountNumber;
#balance;
#pin;
constructor(ownerName, initialBalance, pin) {
this.ownerName = ownerName;
this.#accountNumber = this.#generateAccountNumber();
this.#balance = initialBalance;
this.#pin = pin;
}
// Private method
#generateAccountNumber() {
return "KMP" + Math.floor(100000 + Math.random() * 900000);
}
#validationPin(pin) {
return pin === this.#pin;
}
getBalance(pin) {
if (!this.#validationPin(pin)) return "Wrong PIN!";
return this.#balance;
}
setor(pin, count) {
if (!this.#validationPin(pin)) return "Wrong PIN!";
if (count <= 0) return "Amount must be positive!";
this.#balance += count;
return `Paid $${count.toLocaleString("en-US")}`;
}
// Getter for the account number (read-only)
get accountNumber() {
return this.#accountNumber;
}
}
const account = new CampusAccount("Mike", 500000, "1234");
console.log(account.accountNumber); // "KMP482931"
console.log(account.getBalance("1234")); // 500000
console.log(account.getBalance("9999")); // "Wrong PIN!"
console.log(account.setor("1234", 200000));
// "Paid $200,000"
Class Expression
// Named class expression
const Campus = class CampusInternal {
constructor(name) {
this.name = name;
}
};
// Anonymous class expression
const Room = class {
constructor(name, capacity) {
this.name = name;
this.capacity = capacity;
this.occupants = 0;
}
enter() {
if (this.occupants < this.capacity) {
this.occupants++;
return true;
}
return false;
}
get info() {
return `${this.name}: ${this.occupants}/${this.capacity}`;
}
};
const computerLab = new Room("Computer Lab", 30);
computerLab.enter();
computerLab.enter();
console.log(computerLab.info); // "Computer Lab: 2/30"
Code Explanation
| Concept | Explanation |
|---|---|
constructor() |
Special method called when new is executed |
get/set |
Access a property like a plain value, but implemented behind the scenes with a method |
static |
A method/property belonging to the class, not the instance |
#field |
Private field — cannot be accessed from outside the class |
| Class expression | A class defined as an expression, can be anonymous |
Class Hierarchy at a Campus
AcademicSystem (static)
├── Student (instance methods)
│ ├── getInfo()
│ ├── addCourse()
│ └── getGPA()
├── Lecturer (instance methods)
│ ├── getInfo()
│ ├── getJadwal()
│ └── beriGrade()
├── Course (getters/setters)
│ ├── get info()
│ ├── set grade()
│ └── getGrade()
└── CampusAccount (private fields)
├── #balance
├── setor()
└── tarik()
Real-World Examples
A class registration system for a new semester:
class CampusClass {
constructor(className, lecturer, capacity) {
this.className = className;
this.lecturer = lecturer;
this.capacity = capacity;
this.students = [];
}
get remaining() {
return this.capacity - this.students.length;
}
get isFull() {
return this.remaining <= 0;
}
register(student) {
if (this.isFull) {
return { success: false, message: "Class is full!" };
}
if (this.students.find(m => m.studentId === student.studentId)) {
return { success: false, message: "Student is already registered!" };
}
this.students.push(student);
return {
success: true,
message: `${student.name} successfully enrolled in ${this.className}`
};
}
printRoster() {
console.log(`\n=== ${this.className} | Lecturer: ${this.lecturer} ===`);
console.log(`Slots: ${this.remaining} remaining\n`);
this.students.forEach((m, i) => {
console.log(`${i + 1}. ${m.name} (${m.studentId})`);
});
}
}
// Registration
const myClassA = new CampusClass("CS-A", "Dr. John", 3);
const result1 = myClassA.register({ name: "Mike", studentId: "001" });
console.log(result1.message); // "Mike successfully enrolled in CS-A"
const result2 = myClassA.register({ name: "Anna", studentId: "002" });
const result3 = myClassA.register({ name: "John", studentId: "003" });
const result4 = myClassA.register({ name: "Amy", studentId: "004" });
console.log(result4.message); // "Class is full!"
myClassA.printRoster();
Another Example: Calculating GPA with a Static Method
class GPACalculator {
static readonlyMap = {
"A": 4.0, "A-": 3.7, "B+": 3.3, "B": 3.0,
"B-": 2.7, "C+": 2.3, "C": 2.0, "D": 1.0, "E": 0
};
static calculateGPA(gradesPerSemester) {
let totalWeight = 0;
let totalCredits = 0;
for (const semester of gradesPerSemester) {
for (const { code, credits, letter } of semester.course) {
const weight = GPACalculator.readonlyMap[letter] ?? 0;
totalWeight += weight * credits;
totalCredits += credits;
}
}
return totalCredits > 0 ? (totalWeight / totalCredits).toFixed(2) : 0;
}
}
const dataSemester = [
{
semester: 1,
course: [
{ code: "IF101", credits: 3, letter: "A" },
{ code: "IF102", credits: 3, letter: "B+" },
{ code: "MA101", credits: 2, letter: "A-" }
]
}
];
console.log(GPACalculator.calculateGPA(dataSemester)); // "3.71"
Conclusion
- A class is a blueprint for creating objects with consistent structure and behavior.
- A constructor runs automatically when an object is created with
new. - Getters/Setters enable property access with validation behind the scenes.
- Static methods belong to the class, not the instance — ideal for utilities and factories.
- Private fields
#ensure sensitive data cannot be accessed from outside the class.