Thuộc tính và phương thức private của class trong JavaScript

Cập nhật ngày 31/03/2022

Một trong những tính chất quan trọng của lập trình hướng đối tượng là tính đóng gói - Encapsulation.

Hiểu đơn giản, tính đóng gói là khả năng che giấu thông tin của đối tượng với môi trường bên ngoài. Việc cho phép môi trường bên ngoài tác động lên các dữ liệu bên trong của đối tượng hoàn toàn tùy thuộc vào người lập trình.

Để làm được điều này, các ngôn ngữ lập trình như C++, Java,... hỗ trợ từ khóa privateprotected giúp hạn chế phạm vi sử dụng của các thuộc tính và phương thức trong class.

Tuy nhiên, JavaScript lại không hỗ trợ các từ khóa này. Vậy thì làm sao để khai báo và sử dụng các phương thức private (protected), thuộc tính private (protected) trong JavaScript?

Đặt bài toán

Trước khi đi vào chi tiết cách triển khai phương thức private (protected), thuộc tính private (protected) trong JavaScript, mình đặt ra bài toán minh họa về bình nước như sau:

class WaterBottle {
  waterAmount = 0; // lượng nước trong bình

  constructor(volume) {
    this.volume = volume; // thế tích của bình
  }
}

// tạo mới bình nước
let waterBottle = new WaterBottle(100);

// gán giá trị nước
waterBottle.waterAmount = 10;

Trong đoạn code trên, mình định nghĩa class WaterBottle bao gồm:

  • Thuộc tính waterAmount là lượng nước trong bình.
  • Thuộc tính volume là thể tích của bình.

Với cách định nghĩa như trên, thuộc tính waterAmountvolume là hoàn toàn public. Nói cách khác, bạn có thể dễ dàng get/set giá trị cho chúng từ bên ngoài class WaterBottle mà không bị giới hạn gì.

waterBottle.waterAmount = 20;
waterBottle.volume = 200;

Tuy nhiên, thực tế là luôn có giới hạn. Mình mong muốn giá trị của volume không đổi sau khi tạo đối tượng. Còn thuộc tính waterAmount luôn không âm và không vượt quá volume.

Vì vậy, mình muốn các thuộc tính trên được bảo vệ để có thể dễ dàng kiểm soát việc thay đổi chúng.

Định nghĩa thuộc tính protected và phương thức protected

Như mình đã nói ở trên, JavaScript không hỗ trợ từ khóa protected. Tuy nhiên, có một cách mà nhiều lập trình viên ngầm định với nhau là sử dụng dấu gạch dưới _ để biểu diễn phương thức protected và thuộc tính protected.

Với cách này, đoạn code trên có thể sửa thành như sau:

class WaterBottle {
  _waterAmount = 0; // lượng nước trong bình

  get waterAmount() {
    return this._waterAmount;
  }

  set waterAmount(value) {
    if (value < 0) value = 0;
    if (value > this._volume) value = this._volume;
    this._waterAmount = value;
  }

  get volume() {
    return this._volume;
  }

  constructor(volume) {
    this._volume = volume; // thế tích của bình
  }
}

let waterBottle = new WaterBottle(100); // tạo mới bình nước

waterBottle.waterAmount = -10; // gán giá trị nước
console.log(waterBottle.waterAmount); // 0

waterBottle.waterAmount = 200; // gán giá trị nước
console.log(waterBottle.waterAmount); // 100

waterBottle.waterAmount = 50; // gán giá trị nước
console.log(waterBottle.waterAmount); // 50

Trong đoạn code trên, mình đã đổi tên các thuộc tính thành _waterAmount_volume.

Giờ đây, việc get/set giá trị của waterAmount đều thông qua hàm get và set. Trong hàm set waterAmount(), mình có thêm các điều kiện ràng buộc để đảm bảo giá trị của waterAmount luôn không âm và không vượt quá volume.

Đối với _volume, vì mình mong muốn thuộc tính này không thay đổi sau khi khởi tạo đối tượng, nên mình chỉ triển khai hàm get volume chứ không viết hàm set.

Chú ý:

► Mình muốn nhấn mạnh lại rằng, việc get/set giá trị cho các thuộc tính _waterAmount_volume là hoàn toàn có thể. Tuy nhiên, việc này là không khuyến khích vì có thể gây ra lỗi logic sau này.

waterBottle._waterAmount = -10; // gán giá trị nước
waterBottle._volume = 0; // thay đổi thế tích

► Ngoài cách sử dụng hàm getter và setter, bạn có thể viết hàm bình thường dạng get...set... như sau:

class WaterBottle {
  _waterAmount = 0; // lượng nước trong bình

  getWaterAmount() {    return this._waterAmount;  }  setWaterAmount(value) {    if (value < 0) value = 0;    if (value > this._volume) value = this._volume;    this._waterAmount = value;  }  getVolume() {    return this._volume;  }
  constructor(volume) {
    this._volume = volume; // thế tích của bình
  }
}

let waterBottle = new WaterBottle(100); // tạo mới bình nước
waterBottle.setWaterAmount(50);console.log(waterBottle.getWaterAmount()); // 50

Cách viết này thường dài dòng hơn cách sử dụng getter/setter, tuy nhiên lại linh động hơn.

Vì cách sử dụng getter/setter, bạn chỉ truyền được một tham số. Còn khi viết hàm bình thường, bạn có thể truyền số lượng tham số tùy ý.

► Các thuộc tính, phương thức protected với _ như trên không khác gì các thuộc tính, phương thức thông thường về mặt logic. Vì vậy, chúng có thể được truy cập từ các class kế thừa thông qua prototype hay từ khóa extend.

Định nghĩa thuộc tính private và phương thức private

Để định nghĩa thuộc tính private và phương thức private trong class JavaScript, bạn chỉ cần thêm kí tự # trước tên thuộc tính hay phương thức.

Chú ý: thuộc tính private hay phương thức private chưa support hoàn toàn trên các trình duyệt, bạn có thể tham khảo thêm tại đây: CanIUse - JavaScript classes: Private class fields

Áp dụng cách này vào đoạn code trên ta có kết quả như sau:

class WaterBottle {
  #waterAmount = 0; // lượng nước trong bình
  #volume = 0; // thế tích của bình

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

  set waterAmount(value) {
    if (value < 0) value = 0;
    if (value > this.#volume) value = this.#volume;
    this.#waterAmount = value;
  }

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

  constructor(volume) {
    this.#volume = volume;
  }
}

let waterBottle = new WaterBottle(100); // tạo mới bình nước
console.log(waterBottle.#volume);
// Lỗi: Private field '#volume' must be declared in an enclosing class

Trong đoạn code trên, mình đã thay đổi _waterAmount_volume thành #waterAmount#volume. Lúc này, các thuộc tính đã hoàn toàn private. Nếu bạn cố tình truy cập các thuộc tính này từ bên ngoài class thì sẽ gặp lỗi dạng như trên:

Private field '#volume' must be declared in an enclosing class

Đặc biệt, với thuộc tính private và phương thức private, bạn không thể truy cập được chúng từ class kế thừa, ví dụ:

class ExtendedWaterBottle extends WaterBottle {
  constructor(volume) {
    super(volume);
  }

  debug() {
    console.log(this.#volume);
  }
}

let waterBottle = new ExtendedWaterBottle(100); // tạo mới bình nước
console.log(waterBottle.#volume);
// Lỗi: Private field '#volume' must be declared in an enclosing class

Chú ý: thuộc tính private và phương thức private không thể truy cập thông qua cách sử dụng this[name], ví dụ:

class WaterBottle {
  #volume = 0; // lượng nước trong bình

  debug() {
    const fieldName = "#volume";
    console.log("this[fieldName]", this[fieldName]);
    console.log("this.#volume", this.#volume);
  }

  constructor(volume) {
    this.#volume = volume;
  }
}

let waterBottle = new WaterBottle(100);
waterBottle.debug();
// this[fieldName] undefined
// this.#volume 100

Tổng kết

Tính đóng gói là một trong bốn tính chất quan trọng của lập trình hướng đối tượng. Việc sử dụng tính đóng gói giúp tách biệt phần triển khai code với phần sử dụng bên ngoài.

Điều này đặc biệt hữu ích khi bạn xây dựng các module và thư viện. Khi mà người sử dụng code không cần biết đến phần triển khai bên trong, giúp việc sử dụng code trở nên đơn giản hơn.

Để áp dụng tính chất đóng gói vào class trong JavaScript, bạn có thể sử dụng một trong hai cách sau:

  • Thuộc tính protected và phương thức protected: sử dụng kí tự _ trước tên thuộc tính hay phương thức (quy định ngầm).
  • Thuộc tính private và phương thức private: sử dụng kí tự # trước tên thuộc tính hay phương thức (trình duyệt hỗ trợ).

Tham khảo: Private and protected properties and methods

★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé:

Thuộc tính và phương thức static của class trong JavaScript
Kế thừa built-in class trong JavaScript
Chia sẻ:

Bình luận