Cách sử dụng Lớp (Classes) trong JavaScript

Javascript nâng cao | by Học Javascript

Trong JavaScript, Lớp (Class) là một khái niệm quan trọng được giới thiệu từ ES6, giúp lập trình hướng đối tượng trở nên rõ ràng và dễ quản lý hơn. Trước đây, JavaScript chủ yếu sử dụng hàm tạo (constructor functions) và prototype để tạo và kế thừa đối tượng. Tuy nhiên, với sự ra đời của class, việc định nghĩa và mở rộng các đối tượng đã trở nên trực quan hơn, giúp mã nguồn dễ đọc và bảo trì.

Class đóng vai trò quan trọng trong việc tổ chức mã nguồn, tái sử dụng code, và mở rộng chương trình. Việc hiểu cách sử dụng class giúp lập trình viên viết code có cấu trúc tốt hơn, dễ dàng quản lý các đối tượng phức tạp và hỗ trợ các mô hình thiết kế hướng đối tượng (OOP) hiệu quả hơn trong JavaScript.

Class trong JavaScript là gì?

Class trong JavaScript là một khuôn mẫu (blueprint) để tạo ra các đối tượng. Nó giúp tổ chức mã nguồn theo mô hình hướng đối tượng (OOP), giúp việc quản lý và mở rộng chương trình trở nên dễ dàng hơn.

So sánh class với constructor function (hàm tạo truyền thống)

Trước khi class được giới thiệu trong ES6, JavaScript sử dụng constructor function và prototype để tạo đối tượng và kế thừa phương thức. Tuy nhiên, cách này có cú pháp phức tạp và ít trực quan.

Ví dụ sử dụng constructor function:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

// Thêm phương thức vào prototype
Person.prototype.greet = function () {
    console.log(`Xin chào, tôi là ${this.name}`);
};

const person1 = new Person("An", 25);
person1.greet(); // Xin chào, tôi là An

Sử dụng class, cú pháp sẽ gọn gàng hơn:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log(`Xin chào, tôi là ${this.name}`);
    }
}

const person2 = new Person("An", 25);
person2.greet(); // Xin chào, tôi là An

Ưu điểm của class:

  • Cú pháp trực quan, dễ đọc.

  • Dễ dàng mở rộng với tính kế thừa.

  • Tránh thao tác trực tiếp với prototype, giúp code gọn gàng hơn.

Cách khai báo một class trong JavaScript

Trong JavaScript, có thể khai báo một class bằng từ khóa class, sau đó định nghĩa constructor và các phương thức bên trong.

Cấu trúc cơ bản của một class

class ClassName {
    constructor(parameters) {
        // Khởi tạo thuộc tính
    }

    methodName() {
        // Định nghĩa phương thức
    }
}

Ví dụ minh họa về cách khai báo class

Dưới đây là một ví dụ về class Car có thuộc tính brandspeed, cùng với phương thức để tăng tốc:

class Car {
    constructor(brand, speed) {
        this.brand = brand;
        this.speed = speed;
    }

    accelerate(amount) {
        this.speed += amount;
        console.log(`${this.brand} tăng tốc lên ${this.speed} km/h`);
    }
}

const car1 = new Car("Toyota", 80);
car1.accelerate(20); // Toyota tăng tốc lên 100 km/h

Điểm cần lưu ý:

  • Constructor được gọi khi tạo một đối tượng mới từ class.

  • this dùng để tham chiếu đến đối tượng hiện tại.

  • Không cần sử dụng function khi định nghĩa phương thức trong class.

Constructor trong class trong JavaScript

Phương thức constructor() là một phương thức đặc biệt trong class, được tự động gọi khi một đối tượng mới được tạo. Nó được sử dụng để khởi tạo các thuộc tính của đối tượng.

Vai trò của constructor() trong việc khởi tạo đối tượng

  • Gán giá trị ban đầu cho các thuộc tính của đối tượng.

  • Giúp tự động thiết lập trạng thái của đối tượng khi nó được tạo ra.

  • Có thể nhận tham số để khởi tạo đối tượng với dữ liệu cụ thể.

Ví dụ minh họa về constructor()

class Animal {
    constructor(name, sound) {
        this.name = name;
        this.sound = sound;
    }

    makeSound() {
        console.log(`${this.name} kêu: ${this.sound}`);
    }
}

const cat = new Animal("Mèo", "Meo Meo");
cat.makeSound(); // Mèo kêu: Meo Meo

Điểm cần lưu ý:

  • constructor() được gọi tự động khi sử dụng new để tạo đối tượng.

  • Nếu một class không có constructor(), JavaScript sẽ tạo một constructor mặc định.

Tạo đối tượng từ class

Sử dụng từ khóa new để tạo một instance của class

Từ khóa new được sử dụng để tạo một instance (thể hiện) mới từ class. Khi gọi new ClassName(), các bước sau xảy ra:

  • Một đối tượng mới được tạo.

  • Phương thức constructor() của class được gọi.

  • Thuộc tính của đối tượng được khởi tạo.

  • Trả về đối tượng mới đã được khởi tạo.

Gán giá trị cho thuộc tính thông qua constructor

Khi tạo một đối tượng mới, ta có thể truyền các giá trị vào constructor để gán cho các thuộc tính.

Ví dụ minh họa về tạo đối tượng từ class

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    introduce() {
        console.log(`Xin chào, tôi là ${this.name}, ${this.age} tuổi.`);
    }
}

const person1 = new Person("An", 25);
person1.introduce(); // Xin chào, tôi là An, 25 tuổi.

const person2 = new Person("Bình", 30);
person2.introduce(); // Xin chào, tôi là Bình, 30 tuổi.

Lợi ích của việc sử dụng constructor trong class:

  • Giúp tạo đối tượng một cách linh hoạt.

  • Dễ dàng khởi tạo nhiều đối tượng với các thuộc tính khác nhau.

  • Giúp mã nguồn dễ đọc và bảo trì hơn.

Phương thức (Method) trong class trong JavaScript

Cách khai báo phương thức trong class

Phương thức (method) trong class là các hàm được định nghĩa bên trong class, giúp thực hiện các hành động trên đối tượng.

  • Không cần từ khóa function khi khai báo.

  • Có thể truy cập thuộc tính của class thông qua this.

Ví dụ: Khai báo phương thức trong class

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    introduce() {
        console.log(`Xin chào, tôi là ${this.name}, ${this.age} tuổi.`);
    }
}

Gọi phương thức từ một đối tượng

Sau khi tạo một đối tượng từ class, ta có thể gọi phương thức bằng cú pháp:

objectName.methodName();
Ví dụ: Gọi phương thức từ một đối tượng
const person1 = new Person("An", 25);
person1.introduce(); // Xin chào, tôi là An, 25 tuổi.

const person2 = new Person("Bình", 30);
person2.introduce(); // Xin chào, tôi là Bình, 30 tuổi.

Lợi ích của phương thức trong class:

  • Giúp đóng gói logic liên quan đến đối tượng.

  • Dễ dàng tái sử dụng và mở rộng class.

  • Giúp code rõ ràng, dễ hiểu hơn.

Thuộc tính và phương thức tĩnh (Static properties and methods)

Khái niệm static trong JavaScript

  • Thuộc tính và phương thức tĩnh (static) thuộc về class, không thuộc về instance của class.

  • Dùng từ khóa static để khai báo.

  • Chỉ có thể gọi thông qua class, không thể gọi từ instance.​

Ví dụ minh họa về static

Phương thức tĩnh (static method)

class MathUtil {
    static add(a, b) {
        return a + b;
    }
}

console.log(MathUtil.add(5, 10)); // 15

Giải thích:

  • add() là một phương thức tĩnh, được gọi qua MathUtil.add(), không cần tạo instance của class.

Thuộc tính tĩnh (static property)

class Config {
    static appName = "My Application";
    static version = "1.0.0";
}

console.log(Config.appName); // My Application
console.log(Config.version); // 1.0.0

Giải thích:

  • appNameversionthuộc tính tĩnh, có thể truy cập trực tiếp qua Config.appName.

So sánh phương thức thường và phương thức tĩnh

class Counter {
    constructor() {
        this.count = 0;
    }

    increase() { // Phương thức thông thường
        this.count++;
        console.log(`Count: ${this.count}`);
    }

    static reset() { // Phương thức tĩnh
        console.log("Counter reset!");
    }
}

const counter1 = new Counter();
counter1.increase(); // Count: 1
counter1.increase(); // Count: 2

Counter.reset(); // Counter reset!

Giải thích:

  • increase() là phương thức thông thường, chỉ có thể gọi từ instance (counter1.increase()).

  • reset()phương thức tĩnh, chỉ có thể gọi từ class (Counter.reset()).

Kế thừa trong class (Class Inheritance) trong JavaScript

Kế thừa là một tính năng quan trọng trong lập trình hướng đối tượng (OOP), giúp một class có thể mở rộng từ một class khác.

  • Lớp con (Subclass) kế thừa lớp cha (Superclass), giúp tái sử dụng mã nguồn.

  • Cho phép ghi đè (override) các phương thức của lớp cha.

  • Giúp mở rộng và tối ưu hóa code, tránh lặp lại (DRY - Don't Repeat Yourself).

Sử dụng từ khóa extends để kế thừa từ một class khác

Trong JavaScript, từ khóa extends giúp một class kế thừa thuộc tính và phương thức của một class khác.

Ví dụ: Kế thừa trong class
class Animal {
    constructor(name) {
        this.name = name;
    }

    makeSound() {
        console.log("Animal is making a sound...");
    }
}

class Dog extends Animal {
    bark() {
        console.log(`${this.name} đang sủa: Gâu gâu!`);
    }
}

const dog = new Dog("Lucky");
dog.makeSound(); // Animal is making a sound...
dog.bark();      // Lucky đang sủa: Gâu gâu!

Giải thích:

  • Dog kế thừa từ Animal, nên có thể sử dụng phương thức makeSound() của Animal.

  • Dog có thêm phương thức riêng bark().

Sử dụng super() để gọi constructor của lớp cha

  • Trong constructor của lớp con, ta phải dùng super() để gọi constructor của lớp cha.

  • Nếu không gọi super(), sẽ bị lỗi khi truy cập this.

Ví dụ: Dùng super() để gọi constructor lớp cha
class Animal {
    constructor(name) {
        this.name = name;
    }

    makeSound() {
        console.log("Animal is making a sound...");
    }
}

class Cat extends Animal {
    constructor(name, color) {
        super(name); // Gọi constructor của Animal
        this.color = color;
    }

    describe() {
        console.log(`${this.name} là một con mèo màu ${this.color}.`);
    }
}

const cat = new Cat("Mimi", "trắng");
cat.makeSound();  // Animal is making a sound...
cat.describe();   // Mimi là một con mèo màu trắng.

Giải thích:

  • Cat kế thừa Animal và có thêm thuộc tính color.

  • super(name) gọi constructor của Animal, giúp Cat khởi tạo name.

Getter và Setter trong class JavaScript

  • getter (get): Lấy giá trị của thuộc tính theo cách tùy chỉnh.

  • setter (set): Gán giá trị cho thuộc tính với logic kiểm tra.

  • Giúp đóng gói dữ liệu và kiểm soát truy cập thuộc tính.

Ví dụ minh họa về getter và setter

Dùng get để truy xuất thuộc tính theo cách tùy chỉnh

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

const person = new Person("An", "Nguyễn");
console.log(person.fullName); // An Nguyễn

Giải thích:

  • fullName không phải là một thuộc tính thực sự, mà được tính toán từ firstNamelastName.

  • Gọi person.fullName sẽ tự động kích hoạt getter.

Dùng set để kiểm tra dữ liệu trước khi gán giá trị

class User {
    constructor(username) {
        this._username = username;
    }

    get username() {
        return this._username;
    }

    set username(newUsername) {
        if (newUsername.length < 3) {
            console.log("Tên người dùng phải có ít nhất 3 ký tự.");
        } else {
            this._username = newUsername;
        }
    }
}

const user = new User("admin");
console.log(user.username); // admin

user.username = "A"; // Tên người dùng phải có ít nhất 3 ký tự.
user.username = "Alice";
console.log(user.username); // Alice

Giải thích:

  • set username(newUsername) kiểm tra độ dài trước khi gán giá trị.

  • Nếu tên quá ngắn, in cảnh báo thay vì cập nhật.

  • _usernamebiến private giả lập, giúp tránh thay đổi trực tiếp.

Dùng gettersetter để xử lý dữ liệu

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    get area() {
        return this.width * this.height;
    }

    set area(value) {
        console.log("Không thể gán trực tiếp diện tích!");
    }
}

const rect = new Rectangle(10, 5);
console.log(rect.area); // 50

rect.area = 100; // Không thể gán trực tiếp diện tích!

Giải thích:

  • get area() tính diện tích từ width * height.

  • set area(value) chỉ in cảnh báo, ngăn việc gán giá trị trực tiếp.

Private và Public trong Class JavaScript

Đóng gói (Encapsulation) là nguyên tắc trong lập trình hướng đối tượng giúp ẩn thông tin nội bộ của một đối tượng, chỉ cung cấp những gì cần thiết ra bên ngoài.

JavaScript ES6+ hỗ trợ đóng gói thông qua thuộc tính private (#), giúp bảo vệ dữ liệu khỏi truy cập trực tiếp từ bên ngoài class.

Cách khai báo thuộc tính private bằng dấu #

  • Trước ES6, lập trình viên thường dùng dấu gạch dưới _ để giả lập thuộc tính private, nhưng cách này không thực sự ngăn chặn truy cập.

  • ES2020 giới thiệu dấu # để tạo thuộc tính private thực sự.

Ví dụ: So sánh thuộc tính private (#) và thuộc tính công khai (public)

class User {
    #password;  // Thuộc tính private

    constructor(username, password) {
        this.username = username;  // Public
        this.#password = password; // Private
    }

    getPassword() {
        return this.#password; // Có thể truy cập từ bên trong class
    }
}

const user = new User("Alice", "123456");
console.log(user.username);   // Alice (Public)
console.log(user.getPassword()); // 123456 (Có thể truy cập thông qua phương thức)

console.log(user.#password); //  Lỗi: Cannot access private field '#password'

Giải thích:

  • usernamepublic → có thể truy cập trực tiếp từ đối tượng.

  • #passwordprivate → chỉ có thể truy cập từ bên trong class.

Sự khác biệt giữa private và public properties

Thuộc tính Public Private (#)
Truy cập từ bên ngoài Có thể Không thể
Truy cập từ bên trong class Có thể Có thể
Bảo vệ dữ liệu Không an toàn An toàn
Có thể kế thừa không? Có thể Không thể

Ví dụ minh họa về private và public properties

Private giúp bảo vệ dữ liệu nhạy cảm

class BankAccount {
    #balance;

    constructor(owner, balance) {
        this.owner = owner;
        this.#balance = balance;
    }

    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            console.log(`Nạp ${amount} thành công. Số dư mới: ${this.#balance}`);
        }
    }

    getBalance() {
        return `Số dư của ${this.owner}: ${this.#balance}`;
    }
}

const account = new BankAccount("Bob", 1000);
console.log(account.getBalance()); // Số dư của Bob: 1000
account.deposit(500); // Nạp 500 thành công. Số dư mới: 1500

console.log(account.#balance); //  Lỗi: Cannot access private field '#balance'

Lợi ích: Không ai có thể thay đổi #balance từ bên ngoài, giúp bảo vệ dữ liệu ngân hàng.

Sử dụng getter và setter với private properties

class Person {
    #age;

    constructor(name, age) {
        this.name = name;
        this.#age = age;
    }

    get age() {
        return this.#age;
    }

    set age(newAge) {
        if (newAge > 0) {
            this.#age = newAge;
        } else {
            console.log("Tuổi không hợp lệ!");
        }
    }
}

const person = new Person("David", 25);
console.log(person.age); // 25
person.age = 30; // Cập nhật thành công
console.log(person.age); // 30
person.age = -5; // Tuổi không hợp lệ!

Lợi ích: Getter và setter giúp kiểm soát giá trị trước khi cập nhật.

Tính đa hình (Polymorphism) trong class JavaScript

  • Đa hình (Polymorphism) giúp một phương thức có thể hoạt động khác nhau tùy theo lớp đối tượng cụ thể.

  • Cho phép ghi đè phương thức (Method Overriding) để tùy chỉnh hành vi trong lớp con.

Ghi đè phương thức (Method Overriding) trong class

  • Khi một class con định nghĩa lại phương thức đã có trong class cha, ta gọi đó là Method Overriding.

  • Phương thức của lớp con sẽ thay thế phương thức của lớp cha khi được gọi trên đối tượng của lớp con.

  • Dùng super.methodName() để gọi phương thức gốc của lớp cha.

Ví dụ: Ghi đè phương thức trong class

class Animal {
    speak() {
        console.log("Động vật phát ra âm thanh...");
    }
}

class Dog extends Animal {
    speak() {
        console.log("Chó sủa: Gâu gâu!");
    }
}

class Cat extends Animal {
    speak() {
        console.log("Mèo kêu: Meo meo!");
    }
}

const dog = new Dog();
const cat = new Cat();

dog.speak(); // Chó sủa: Gâu gâu!
cat.speak(); // Mèo kêu: Meo meo!

Giải thích:

  • speak() của Animal được ghi đè bởi speak() của DogCat.

  • Mỗi đối tượng có cách thực hiện khác nhau dù cùng gọi speak().

Ví dụ nâng cao: Sử dụng super() để gọi phương thức của lớp cha

class Vehicle {
    drive() {
        console.log("Phương tiện đang di chuyển...");
    }
}

class Car extends Vehicle {
    drive() {
        super.drive(); // Gọi phương thức của Vehicle
        console.log("Xe hơi chạy trên đường!");
    }
}

const myCar = new Car();
myCar.drive();
// Phương tiện đang di chuyển...
// Xe hơi chạy trên đường!

Giải thích:

  • super.drive() gọi phương thức drive() của Vehicle.

  • Sau đó, Car thêm hành vi mới.

Tính năng Class Cha Class Con (Kế thừa)
Ghi đè phương thức Có thể có phương thức mặc định Có thể ghi đè
super.methodName() Không cần dùng Dùng để gọi phương thức gốc
Hành vi của phương thức Mặc định cho tất cả các lớp con Có thể thay đổi tùy lớp con

Kết bài

Lớp (Class) trong JavaScript mang đến một cách tiếp cận rõ ràng và mạnh mẽ cho lập trình hướng đối tượng. Nhờ có class, việc tổ chức mã nguồn trở nên dễ đọc, dễ quản lý, giúp tái sử dụng và mở rộng ứng dụng một cách hiệu quả.

Các tính năng như constructor, phương thức, kế thừa, getter & setter, thuộc tính private, phương thức tĩnh và tính đa hình giúp lập trình viên xây dựng các ứng dụng linh hoạt, bảo mật và dễ bảo trì.

Bằng cách áp dụng đúng cách các kỹ thuật trên, bạn có thể viết mã nguồn hiệu quả, tối ưu và chuyên nghiệp hơn. Nếu bạn đang phát triển ứng dụng lớn hoặc muốn áp dụng lập trình hướng đối tượng vào JavaScript, class chính là công cụ không thể bỏ qua!

Bài viết liên quan