Tìm hiểu cách sử dụng lớp (Classes) trong JavaScript

Javascript căn bản | by Học Javascript

Trong JavaScript, trước ES6, lập trình viên thường sử dụng Constructor Function để tạo đối tượng và mô phỏng lập trình hướng đối tượng (OOP). Tuy nhiên, cú pháp này không trực quan và có thể gây nhầm lẫn. Để khắc phục điều đó, JavaScript ES6 đã giới thiệu Classes – một cách khai báo dễ đọc, rõ ràng và gần gũi hơn với lập trình hướng đối tượng trong các ngôn ngữ như Java hay C++.

Classes trong JavaScript giúp tổ chức mã nguồn tốt hơn, hỗ trợ tính kế thừa (inheritance), tính đóng gói (encapsulation) và tính trừu tượng (abstraction). Nhờ đó, chúng trở thành một công cụ mạnh mẽ để xây dựng các ứng dụng hiện đại, đặc biệt trong các framework như React.js, Vue.js hay khi làm việc với mô hình OOP.

Trong bài viết này, mình sẽ tìm hiểu cách khai báo và sử dụng Classes, tìm hiểu các tính năng quan trọng như constructor, kế thừa, phương thức tĩnh (static methods), getter/setter, cũng như một số lưu ý khi làm việc với Class trong JavaScript.

Classes trong JavaScript

Định nghĩa Classes trong JavaScript

Trong JavaScript, Class là một cú pháp mới được giới thiệu từ ES6 để định nghĩa các đối tượng và làm việc theo mô hình lập trình hướng đối tượng (OOP). Classes không phải là một kiểu dữ liệu mới, mà chỉ là một cách viết dễ hiểu hơn so với việc sử dụng Constructor Function.

Class trong JavaScript giúp định nghĩa các đối tượng có thuộc tính (properties) và phương thức (methods), đồng thời hỗ trợ các tính năng như kế thừa (inheritance), phương thức tĩnh (static methods) và getter/setter.

Ví dụ về một Class đơn giản:

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

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

// Tạo một đối tượng từ Class
const person1 = new Person("Kiên", 25);
person1.greet(); // Output: Xin chào, tôi là Kiên và tôi 25 tuổi.

Trong đoạn code trên:

  • class Person {} khai báo một lớp Person.
  • constructor(name, age) giúp khởi tạo thuộc tính nameage cho mỗi đối tượng được tạo.
  • Phương thức greet() giúp in ra thông tin của đối tượng.

Lịch sử ra đời của Classes trong JavaScript

Trước khi ES6 ra đời, JavaScript sử dụng Constructor Function để mô phỏng lập trình hướng đối tượng. Điều này có thể gây khó hiểu do cú pháp không trực quan.

Ví dụ về Constructor Function trước ES6:

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

Person.prototype.greet = function() {
    console.log(`Xin chào, tôi là ${this.name} và tôi ${this.age} tuổi.`);
};

const person1 = new Person("Kiên", 25);
person1.greet();

Dù cách này hoạt động tốt, nhưng việc sử dụng prototype để định nghĩa phương thức không trực quan.

Với ES6, JavaScript giới thiệu Class, giúp lập trình viên viết code dễ đọc hơn và có cú pháp gần giống với các ngôn ngữ hướng đối tượng như Java, C++, Python.

Sự khác biệt giữa Classes và Constructor Function

Tiêu chí Constructor Function Class (ES6)
Cú pháp Dài dòng, khó đọc Gọn gàng, dễ hiểu
Cách định nghĩa phương thức Dùng prototype Định nghĩa trực tiếp trong Class
Kế thừa (Inheritance) Dùng Object.create() hoặc prototype Dùng extendssuper()
Hoisting Có thể gọi trước khi khai báo Không thể gọi trước khi khai báo
Strict Mode Không mặc định là strict mode Mặc định chạy trong strict mode

Ví dụ về kế thừa với Constructor Function (trước ES6):

function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(`${this.name} phát ra âm thanh.`);
};

// Kế thừa từ Animal
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const dog1 = new Dog("Buddy", "Labrador");
dog1.speak(); // Output: Buddy phát ra âm thanh.

Ví dụ về kế thừa với Class (ES6):

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

    speak() {
        console.log(`${this.name} phát ra âm thanh.`);
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name); // Gọi constructor của lớp cha
        this.breed = breed;
    }
}

const dog1 = new Dog("Buddy", "Labrador");
dog1.speak(); // Output: Buddy phát ra âm thanh.

Với Class, việc kế thừa dễ dàng hơn nhờ từ khóa extendssuper().

Lợi ích khi sử dụng Classes trong JavaScript

Cú pháp rõ ràng, dễ đọc hơn

  • Code dễ hiểu hơn so với Constructor Function.
  • Dễ bảo trì và mở rộng.

Hỗ trợ kế thừa mạnh mẽ

  • Sử dụng extends để kế thừa lớp cha.
  • Dễ dàng gọi phương thức của lớp cha bằng super().

Tự động thực thi trong chế độ Strict Mode

  • Code trong Class luôn chạy ở chế độ "use strict", giúp tránh các lỗi phổ biến.

Không ảnh hưởng đến hiệu suất

  • Class chỉ là một cú pháp mới, nên hiệu suất tương đương với Constructor Function.

Dễ dàng mở rộng với các tính năng nâng cao

  • Hỗ trợ getter, setter, static methods, private fields, v.v.

Cách khai báo và sử dụng Classes trong JavaScript

Khai báo một Class cơ bản

Cú pháp của Class trong JavaScript được giới thiệu từ ES6 và có cấu trúc gọn gàng, dễ hiểu hơn so với Constructor Function truyền thống.

Cú pháp cơ bản của Class:

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

    methodName() {
        // Định nghĩa phương thức
    }
}
Ví dụ minh họa:
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

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

// Tạo đối tượng từ class
const person1 = new Person("Nam", 30);
person1.greet(); // Output: Xin chào, tôi là Nam và tôi 30 tuổi.

Constructor trong Class

Chức năng của constructor()

  • constructor() là một phương thức đặc biệt trong Class, được gọi ngay khi tạo một đối tượng mới bằng new.
  • Được dùng để khởi tạo giá trị cho các thuộc tính của đối tượng.

Ví dụ minh họa:

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

    displayInfo() {
        console.log(`${this.brand} ${this.model} - Năm ${this.year}`);
    }
}

const car1 = new Car("Toyota", "Corolla", 2022);
car1.displayInfo(); // Output: Toyota Corolla - Năm 2022

Thuộc tính và phương thức trong Class

Khai báo thuộc tính trong constructor()

  • Các thuộc tính được khai báo trong constructor() để đảm bảo mỗi đối tượng có giá trị riêng.

Định nghĩa phương thức trong Class

  • Phương thức có thể được định nghĩa bên ngoài constructor(), không cần từ khóa function.

Ví dụ minh họa:

class Student {
    constructor(name, grade) {
        this.name = name;
        this.grade = grade;
    }

    study() {
        console.log(`${this.name} đang học.`);
    }
}

const student1 = new Student("Linh", "12A1");
student1.study(); // Output: Linh đang học.

Phạm vi truy cập: Public, Private và Protected (ES2020 trở lên)

Public Properties (Thuộc tính công khai)

  • Mặc định, tất cả các thuộc tính trong Class đều là public (có thể truy cập từ bên ngoài).
class Animal {
    constructor(name) {
        this.name = name; // Public
    }
}

const dog = new Animal("Buddy");
console.log(dog.name); // Buddy

Private Properties (Thuộc tính riêng tư - ES2020+)

  • Dùng dấu # trước tên thuộc tính để tạo thuộc tính private (chỉ truy cập được bên trong class).
class BankAccount {
    #balance; // Private

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

    getBalance() {
        return this.#balance;
    }
}

const account = new BankAccount("Hùng", 5000);
console.log(account.getBalance()); // 5000
console.log(account.#balance); //  Lỗi: Cannot access private field

Protected Properties (Không có chính thức trong JavaScript, nhưng có thể giả lập)

  • Quy ước dùng _property để thể hiện thuộc tính "protected" (chỉ nên được truy cập trong class hoặc subclass).
class Employee {
    constructor(name, salary) {
        this.name = name;
        this._salary = salary; // Giả lập protected
    }

    showSalary() {
        console.log(`${this.name} có lương ${this._salary}`);
    }
}

Kế thừa (Inheritance) trong Class

Sử dụng extends để kế thừa từ Class khác

  • Khi một Class kế thừa từ một Class khác, nó có thể sử dụng tất cả thuộc tính và phương thức của lớp cha.

Gọi constructor của lớp cha bằng super()

  • Phương thức super() được gọi trong constructor của lớp con để kế thừa thuộc tính từ lớp cha.

Ghi đè phương thức của lớp cha

  • Lớp con có thể ghi đè (override) phương thức của lớp cha bằng cách định nghĩa lại nó.

Ví dụ minh họa:

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

    speak() {
        console.log(`${this.name} phát ra âm thanh.`);
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name); // Gọi constructor của lớp cha
        this.breed = breed;
    }

    speak() {
        console.log(`${this.name} (loài ${this.breed}) sủa gâu gâu!`);
    }
}

const dog1 = new Dog("Rex", "Labrador");
dog1.speak(); // Output: Rex (loài Labrador) sủa gâu gâu!

Static Methods và Static Properties

static để tạo phương thức và thuộc tính tĩnh

  • Phương thức và thuộc tính static chỉ có thể được gọi từ Class, không từ instance.

Ví dụ minh họa:

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

console.log(MathUtils.add(5, 10)); // Output: 15

Getter và Setter trong Class

getset để kiểm soát thuộc tính

  • Dùng get để lấy giá trị thuộc tính.
  • Dùng set để cập nhật giá trị với kiểm tra hợp lệ.

Ví dụ minh họa:

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

    get name() {
        return this._name;
    }

    set name(newName) {
        if (newName.length < 3) {
            console.log("Tên quá ngắn!");
        } else {
            this._name = newName;
        }
    }
}

const p = new Person("An");
console.log(p.name); // An
p.name = "Te"; // Output: Tên quá ngắn!

Class Expression

Khai báo Class bằng cách gán vào biến

  • Thay vì dùng class ClassName {}, có thể gán Class vào biến.

Sự khác biệt giữa Class Expression và Class Declaration

  • Class Declaration có thể hoisted.
  • Class Expression không thể hoisted.

Ví dụ minh họa:

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

    showBrand() {
        console.log(`Thương hiệu: ${this.brand}`);
    }
};

const myCar = new Car("Honda");
myCar.showBrand(); // Output: Thương hiệu: Honda

Các lưu ý quan trọng khi sử dụng Classes trong JavaScript

Hoisting của Class

Sự khác biệt giữa hoisting của Function và Class

  • Function Declaration được hoisted, nghĩa là có thể gọi trước khi khai báo.
  • Class Declaration không được hoisted, nên nếu sử dụng trước khi khai báo sẽ bị lỗi.

Lỗi khi sử dụng Class trước khi khai báo

const obj = new MyClass(); //  Lỗi: Cannot access 'MyClass' before initialization

class MyClass {
    constructor() {
        console.log("Class được khởi tạo!");
    }
}
  • Lý do: Class thực chất là một "hàm đặc biệt" nhưng không được hoisted như function.

Ví dụ so sánh với Function

// Function được hoisted
sayHello(); //  Hoạt động bình thường

function sayHello() {
    console.log("Xin chào!");
}

// Class không được hoisted
const person = new Person(); //  Lỗi: Cannot access 'Person' before initialization

class Person {
    constructor() {
        this.name = "An";
    }
}

Sự khác biệt giữa Object Literal và Class

Cả Object Literal ({}) và Class (class) đều dùng để tạo đối tượng, nhưng có những điểm khác biệt quan trọng.

Tiêu chí Object Literal ({}) Class (class)
Cú pháp { key: value } class ClassName {}
Tạo đối tượng const obj = {} const obj = new ClassName()
Kế thừa Không có kế thừa thực sự Hỗ trợ kế thừa (extends)
Tính đóng gói Không hỗ trợ private Hỗ trợ private (#)
Dùng khi nào? Đối tượng đơn giản Khi cần mô hình OOP

Khi nào nên dùng Object Literal ({})?

  • Khi chỉ cần lưu trữ một nhóm dữ liệu đơn giản.
  • Khi không cần kế thừa hoặc tái sử dụng nhiều lần.
  • Khi muốn tạo đối tượng một cách nhanh chóng.

Ví dụ Object Literal:

const person = {
    name: "Nam",
    age: 25,
    greet() {
        console.log(`Xin chào, tôi là ${this.name}`);
    }
};

person.greet(); // Output: Xin chào, tôi là Nam

Khi nào nên dùng Class (class)?

  • Khi cần tạo nhiều đối tượng có cùng cấu trúc.
  • Khi muốn sử dụng kế thừa (extends).
  • Khi cần tính đóng gói (private properties).

Ví dụ Class:

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

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

const p1 = new Person("Nam", 25);
p1.greet(); // Output: Xin chào, tôi là Nam

Performance: Class vs Constructor Function

Trong JavaScript, Class được xây dựng dựa trên Constructor Function, nhưng có một số khác biệt về hiệu suất.

Hiệu suất khi sử dụng Class so với Constructor Function

  • Constructor Function thường nhanh hơn một chút vì nó không có các tính năng bổ sung của Class.
  • Class giúp code dễ đọc hơn, nhưng có thể chậm hơn khi tạo nhiều đối tượng.

Khi nào nên dùng Class?

  • Khi làm việc với các đối tượng có nhiều thuộc tính, phương thức, kế thừa.
  • Khi muốn code dễ bảo trì hơn.
  • Khi cần sử dụng tính năng private hoặc static.

Khi nào nên dùng Constructor Function?

  • Khi cần hiệu suất tốt hơn (trong các ứng dụng có số lượng lớn đối tượng).
  • Khi làm việc với ES5 hoặc môi trường cũ không hỗ trợ Class.

Ví dụ Constructor Function:

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

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

const p1 = new Person("Hải", 30);
p1.greet(); // Output: Xin chào, tôi là Hải

So sánh hiệu suất Class vs Constructor Function

console.time("Class");
class PersonClass {
    constructor(name) {
        this.name = name;
    }
}
for (let i = 0; i < 1000000; i++) {
    new PersonClass("Nam");
}
console.timeEnd("Class");

console.time("Constructor Function");
function PersonFunc(name) {
    this.name = name;
}
for (let i = 0; i < 1000000; i++) {
    new PersonFunc("Nam");
}
console.timeEnd("Constructor Function");
  • Constructor Function có thể nhanh hơn một chút, nhưng Class giúp code rõ ràng và dễ bảo trì hơn.
  • Trong hầu hết các trường hợp, sử dụng Class sẽ tốt hơn vì tính dễ đọc và bảo trì quan trọng hơn hiệu suất nhỏ lẻ.

Ứng dụng thực tế của Classes trong JavaScript

Trong JavaScript hiện đại, Classes được sử dụng rộng rãi để xây dựng các ứng dụng có cấu trúc tốt hơn, đặc biệt trong các mô hình hướng đối tượng (OOP), React.js, và thiết kế module. Dưới đây là một số ứng dụng thực tế quan trọng.

Xây dựng mô hình hướng đối tượng (OOP) trong JavaScript

Lập trình hướng đối tượng (OOP - Object-Oriented Programming) giúp quản lý và tổ chức code hiệu quả hơn bằng cách sử dụng các class để đại diện cho đối tượng trong thế giới thực.

Mô hình OOP sử dụng Class

Trong OOP, Class giúp tạo các đối tượng có thuộc tính và phương thức, hỗ trợ tính kế thừa (inheritance), đa hình (polymorphism) và đóng gói (encapsulation).

Ví dụ: Quản lý nhân viên trong công ty

// Định nghĩa lớp cha Employee
class Employee {
    constructor(name, age, salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    getDetails() {
        return `${this.name}, ${this.age} tuổi, lương: ${this.salary}`;
    }
}

// Lớp con Manager kế thừa từ Employee
class Manager extends Employee {
    constructor(name, age, salary, department) {
        super(name, age, salary);
        this.department = department;
    }

    getDetails() {
        return `${super.getDetails()}, quản lý phòng: ${this.department}`;
    }
}

// Sử dụng các class
const emp = new Employee("Nam", 30, 1500);
console.log(emp.getDetails()); 
// Output: Nam, 30 tuổi, lương: 1500

const mgr = new Manager("Hà", 40, 3000, "IT");
console.log(mgr.getDetails()); 
// Output: Hà, 40 tuổi, lương: 3000, quản lý phòng: IT

Lợi ích của OOP với Class:

  • Code rõ ràng, dễ đọc và bảo trì.
  • Tái sử dụng mã nguồn với kế thừa.
  • Đóng gói dữ liệu giúp bảo vệ và kiểm soát truy cập.

Sử dụng Class trong lập trình với React.js

React.js là một thư viện JavaScript phổ biến để xây dựng giao diện người dùng. Trước khi React Hooks ra đời, Class Components là cách chính để xây dựng component trong React.

Ví dụ: Class Component trong React

import React, { Component } from 'react';

class Welcome extends Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
    }

    increment = () => {
        this.setState({ count: this.state.count + 1 });
    };

    render() {
        return (
            <div>
                <h1>Xin chào, {this.props.name}!</h1>
                <p>Count: {this.state.count}</p>
                <button onClick={this.increment}>Tăng</button>
            </div>
        );
    }
}

export default Welcome;

Tính năng của Class Component trong React:

  • Có state và lifecycle methods (componentDidMount, componentWillUnmount, v.v.).
  • Có thể sử dụng this để truy cập thuộc tính và phương thức.
  • Trước khi Hooks xuất hiện, đây là cách chính để quản lý state.

Lưu ý: Hiện nay, React khuyến khích sử dụng Function Components + Hooks, nhưng Class Component vẫn được dùng trong các codebase cũ.

Ứng dụng Class trong thiết kế module và thư viện

JavaScript hỗ trợ lập trình module để tổ chức mã nguồn tốt hơn. Class giúp xây dựng các module linh hoạt và tái sử dụng.

Ví dụ: Tạo module quản lý sản phẩm

Product.js

class Product {
    constructor(name, price) {
        this.name = name;
        this.price = price;
    }

    getInfo() {
        return `Sản phẩm: ${this.name}, Giá: ${this.price} VNĐ`;
    }
}

export default Product;

index.js

import Product from './Product.js';

const product1 = new Product("Laptop", 20000000);
console.log(product1.getInfo()); 
// Output: Sản phẩm: Laptop, Giá: 20000000 VNĐ

Lợi ích của việc dùng Class trong module:

  • Tạo module dễ tái sử dụng.
  • Tổ chức code gọn gàng và dễ bảo trì.
  • Có thể mở rộng bằng cách kế thừa (extends).

Ví dụ minh họa thực tế

Ví dụ: Xây dựng hệ thống đặt hàng trong thương mại điện tử

class Product {
    constructor(name, price) {
        this.name = name;
        this.price = price;
    }
}

class Order {
    constructor() {
        this.items = [];
    }

    addProduct(product) {
        this.items.push(product);
    }

    getTotalPrice() {
        return this.items.reduce((total, item) => total + item.price, 0);
    }

    printOrder() {
        console.log("Chi tiết đơn hàng:");
        this.items.forEach(item => console.log(`- ${item.name}: ${item.price} VNĐ`));
        console.log(`Tổng tiền: ${this.getTotalPrice()} VNĐ`);
    }
}

// Tạo sản phẩm
const product1 = new Product("iPhone 15", 25000000);
const product2 = new Product("MacBook Air", 30000000);

// Tạo đơn hàng
const order = new Order();
order.addProduct(product1);
order.addProduct(product2);

// Hiển thị đơn hàng
order.printOrder();
Kết quả:
Chi tiết đơn hàng:
- iPhone 15: 25000000 VNĐ
- MacBook Air: 30000000 VNĐ
Tổng tiền: 55000000 VNĐ

Lợi ích của việc dùng Class trong ứng dụng thực tế:

  • Dễ mở rộng (có thể thêm phương thức, kế thừa lớp mới).
  • Tái sử dụng code hiệu quả.
  • Tổ chức mã nguồn tốt hơn, giúp bảo trì dễ dàng.

Kết bài

Lớp (Class) trong JavaScript là một tính năng mạnh mẽ được giới thiệu từ ES6, giúp lập trình theo mô hình hướng đối tượng (OOP) trở nên dễ dàng và trực quan hơn. Việc sử dụng Class mang lại nhiều lợi ích như tái sử dụng code, tổ chức mã nguồn chặt chẽ, hỗ trợ kế thừa và đóng gói dữ liệu, giúp xây dựng các ứng dụng có cấu trúc rõ ràng hơn.

Trong thực tế, Class được ứng dụng rộng rãi trong nhiều lĩnh vực như quản lý dữ liệu, phát triển giao diện với React.js, tạo module và thư viện, cũng như trong các hệ thống thương mại điện tử, phần mềm quản lý nhân sự, v.v.

Dù có nhiều ưu điểm, nhưng Class không phải lúc nào cũng là lựa chọn tốt nhất. Trong một số trường hợp, Object Literal hoặc Constructor Function có thể là phương án tối ưu hơn. Vì vậy, việc hiểu rõ cách hoạt động và ứng dụng Class đúng cách sẽ giúp lập trình viên viết code hiệu quả, dễ bảo trì và mở rộng.

Bài viết liên quan