Skillversitas

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.