Học bằng cách đọc theo thứ tự

Private field (#) và đóng gói

Private field và private method dùng # ở đầu tên để chặn đọc, ghi từ ngoài class. Kèm SyntaxError xảy ra trước khi code chạy và khác biệt giữa # với _.

Lưu số dư của thẻ trả trước vào _balance không bảo vệ được nó: chỉ cần ở đâu đó trong code của trang có dòng card._balance = 999999 là giá trị đó được gán thẳng vào. Dấu _ ở đầu tên chỉ là quy ước ngầm hiểu là "đừng truy cập từ bên ngoài"; nó không ngăn được phép gán hay lời gọi nào.

Bài này nói về private fieldprivate method, những thứ không thể đọc hay ghi từ bên ngoài class.

Giấu số dư bên trong class — private field

Giả sử bạn muốn số dư chỉ thay đổi qua charge, phương thức áp giới hạn 20000 yên cho mỗi lần nạp. Với _balance, nếu code của màn hình hoàn tiền viết card._balance += 30000 thì cũng không có gì ngăn lại, và số tiền vượt giới hạn vẫn vào số dư.

Private field (field trong class có tên bắt đầu bằng #, chỉ đọc và ghi được bên trong thân class đó) được khai báo là #balance = 0; và dùng dưới dạng this.#balance.

Dấu # là một phần của tên, nên this.balancethis.#balance là hai tên khác nhau.

class PrepaidCard {
  #balance = 0;                              // Số dư. Không đọc hay ghi được từ ngoài class

  charge(amount) {
    if (amount > 20000) {
      throw new Error(`Mỗi lần nạp tối đa 20000 yên: ${amount}`);
    }
    this.#balance += amount;                 // Ở trong class nên thay đổi được
  }

  get balance() { return this.#balance; }    // Chỉ mở getter để đọc
}

const card = new PrepaidCard();
card.charge(5000);                           // charge là cách duy nhất để đổi số dư
card.charge(3000);
console.log(card.balance);                   // 8000
Dòng nào tới được #balance, dòng nào không
Thân class PrepaidCard
  • #balance = 0 — tên chỉ dùng được bên trong thân này
Thân của charge(amount)
  • Nếu amount > 20000 thì throw
  • Chỉ số tiền hợp lệ mới chạy this.#balance += amount
Thân của get balance()
  • return this.#balance — chỉ trả về giá trị, không bao giờ thay đổi nó
Các dòng bên ngoài class
  • card.charge(5000) — thay đổi số dư thông qua charge
  • card.balance — đọc được 8000 thông qua getter
  • Không thể viết tên #balance ở đây
Bạn chỉ viết được #balance bên trong thân class. Với code bên ngoài, charge là cách duy nhất để thay đổi số dư.

Nhờ đóng gói (encapsulation: cách thiết kế gom dữ liệu và code thay đổi dữ liệu đó vào một class, và chỉ cho bên ngoài dùng qua những lối vào đã định), cách duy nhất để thay đổi số dư của PrepaidCard này là charge, và cách duy nhất để đọc nó là getter balance.

Dùng tên # chưa khai báo là SyntaxError

Hãy khai báo tên # trong thân class, như #balance = 0; hoặc #balance;, trước khi dùng. Nếu bạn gán this.#balance = 0; trong constructor mà chưa khai báo, bạn sẽ gặp SyntaxError, dù dòng đó nằm bên trong class. Gán vào một tên # không tạo ra tên đó.

Bạn làm cho số điểm của hội viên chỉ thay đổi được qua việc mua hàng và dùng điểm. PointAccount và orders đã được khai báo sẵn.

① Hãy đổi số điểm thành một field không thể đọc hay ghi từ bên ngoài class.

② Hãy thêm points, chỉ dùng để đọc số điểm.

③ Hãy xử lý lần lượt các đơn hàng và hiển thị số điểm sau mỗi lần.

④ Hãy gán 999 vào tên cũ _points và hiển thị "Số dư: X điểm".

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Viết từ bên ngoài thì lỗi trước khi chạy — SyntaxError

Giả sử bạn muốn đặt số dư thành 999999 từ code của trang để thử nghiệm. Nếu viết card.#balance = 999999 như từng làm với _balance, dòng đó sẽ lỗi. Tệ hơn nữa, ngay cả các dòng phía trên, lẽ ra phải chạy trước, cũng không in ra gì.

Viết card.#balance bên ngoài class sẽ gây ra SyntaxError: Private field '#balance' must be declared in an enclosing class ngay ở bước tải code, giống như khai báo lại một biến let. Thông báo này có nghĩa là "private field phải được khai báo trong class bao quanh nó".

class PrepaidCard {
  #balance = 0;

  charge(amount) { this.#balance += amount; }
  get balance() { return this.#balance; }
}

console.log("Bắt đầu kiểm tra");    // Dòng này cũng không được in ra

const card = new PrepaidCard();
card.charge(5000);

// Với _balance, code chạy từ trên xuống, nhưng tên # làm code dừng ngay ở bước tải
card.#balance = 999999;             // SyntaxError: Private field '#balance' must be declared in an enclosing class
console.log(card.balance);
Phép gán từ bên ngoài đi được tới đâu
card._balance= 999999Chạy từdòng đầu tiênIn ra "Bắt đầukiểm tra"Gán thành công:số dư 999999card.#balance= 999999Kiểm tra cú pháplúc tải codeCó tên # nằmngoài classPhát hiện lỗitrước khi chạy
Hàng trên chạy tới tận dòng gán, và output trước đó vẫn hiện ra. Hàng dưới dừng lại trước khi chạy dòng đầu tiên.

Nếu bạn gặp SyntaxError bắt đầu bằng Private field và không có gì được in ra, hãy tìm tên # được viết bên ngoài class. Dòng nào thay đổi giá trị từ bên ngoài thì hãy viết lại để nó gọi một phương thức của class, chẳng hạn charge.

Code trên trang của cửa hàng đang cộng thêm số lượt dùng còn lại cho phiếu giảm giá tặng khi ghé cửa hàng bằng cách ghi thẳng vào tên #. Hãy đổi nó thành lời gọi phương thức của class. VisitCoupon và coupon đã được khai báo sẵn.

① Hãy dùng phiếu hai lần và hiển thị "Còn X lượt".

② Hãy thêm addUses vào class sao cho số lượt còn lại không bao giờ vượt quá 5.

③ Hãy cộng thêm 2 lượt và hiển thị số lượt còn lại.

④ Hãy cộng thêm 4 lượt nữa và hiển thị số lượt còn lại.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Không cho gọi bước nội bộ từ bên ngoài — private method

Giả sử bạn tách đoạn code trừ số dư ra khỏi pay thành một phương thức thường tên là withdraw. Khi đó code bên ngoài class cũng gọi được card.withdraw(5000), bỏ qua bước kiểm tra số dư trong pay và đẩy số dư xuống dưới 0.

Nếu bạn biến nó thành private method (phương thức được định nghĩa với # ở đầu tên, chỉ gọi được từ các phương thức của cùng class, theo dạng this.#withdraw(price)), bước bạn tách ra sẽ không thể gọi từ bên ngoài. Nếu sau này bạn đổi tên hay tham số của nó, phạm vi cần sửa cũng chỉ nằm trong class.

class PrepaidCard {
  #balance = 3000;

  // Bước chỉ trừ số dư. pay kiểm tra trước xem có đủ tiền không
  #withdraw(amount) { this.#balance -= amount; }

  pay(price) {
    if (price > this.#balance) {
      return `Số dư không đủ: ${price}`;
    }
    this.#withdraw(price);                     // Ở trong class nên gọi được
    return `Số dư sau thanh toán: ${this.#balance}`;
  }
}

const card = new PrepaidCard();
console.log(card.pay(480));                    // Số dư sau thanh toán: 2520
console.log(card.pay(5000));                   // Số dư không đủ: 5000
console.log(typeof card.withdraw);             // undefined (gọi nó sẽ báo lỗi TypeError)
Lời gọi nào tới được #withdraw
#withdraw(amount)chỉ trừ số dưcard.pay(480)card.pay(5000)card.withdraw(5000)480 không vượtsố dư 30005000 lớn hơnsố dư 2520Không cówithdraw (không #)Đi qua #withdraw:2520Không tới được#withdraw: 2520Báo lỗiTypeError
Chỉ cột trái, lời gọi qua được bước kiểm tra, mới tới được bước trừ số dư. Tên withdraw không có # không tồn tại trên instance.

pay(5000) return ngay ở bước kiểm tra số dư, nên không bao giờ tới được #withdraw. Viết card.#withdraw(5000) bên ngoài class là SyntaxError ở bước tải code, giống như #balance; còn TypeError ở cột phải là do gọi một tên khác, tên không có #.

Gọi private method kèm this.

Ngay cả bên trong class, nếu bạn bỏ this. và viết #withdraw(price);, bạn sẽ gặp SyntaxError: Unexpected identifier '#withdraw' trước khi code chạy. Cũng như private field, private method luôn được truy cập dưới dạng this.#withdraw(price).

Bạn tính tiền thuê xe đạp hiện tại và phụ phí gia hạn bằng cùng một công thức. BikeRental và rides đã được khai báo sẵn.

① Hãy thêm calcFee để tính tiền thuê, dưới dạng phương thức không thể gọi từ bên ngoài.

② Hãy dùng ① để thêm property fee trả về tiền thuê hiện tại và phương thức extraFee trả về phụ phí khi gia hạn.

③ Hãy hiển thị "X phút: X yên (thêm 10 phút: +X yên)" cho mọi lượt thuê.

④ Hãy đọc calcFee của lượt thuê đầu tiên từ bên ngoài và hiển thị kiểu của nó.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Những gì còn lại trong JSON — _ và #

Giả sử bạn đổi class thẻ trả trước từ _balance sang #balance. Việc đó chặn được phép gán từ bên ngoài, nhưng nếu bạn đang lưu instance dưới dạng chuỗi bằng JSON.stringify, số dư sẽ biến mất khỏi dữ liệu được lưu sau khi đổi.

Tên có _ ở đầu chỉ là một property bình thường tình cờ bắt đầu bằng _, nên nó xuất hiện trong JSON.stringify. Field # không phải là key của property, nên bị bỏ ra khỏi kết quả. Getter được định nghĩa trên class chứ không phải trên instance, nên cũng không phải là key, và balance không xuất hiện.

// Trước khi đổi: property bình thường có _ ở đầu
class LegacyPrepaidCard {
  constructor() { this._balance = 3000; }
}

// Sau khi đổi: private field có # và getter để đọc nó
class PrepaidCard {
  #balance = 3000;
  get balance() { return this.#balance; }
}

const legacyCard = new LegacyPrepaidCard();
const prepaidCard = new PrepaidCard();

console.log(JSON.stringify(legacyCard));                         // {"_balance":3000}
console.log(JSON.stringify(prepaidCard));                        // {}
console.log(JSON.stringify({ balance: prepaidCard.balance }));   // {"balance":3000}
JSON.stringify đọc những gì
prepaidCard (instance có #balance)
Key của property
  • Không có key nào (get balance() là phương thức viết trên class)
  • JSON.stringify(prepaidCard) cho ra {}
Private field
  • #balance — 3000
  • Chỉ các dòng bên trong PrepaidCard mới đọc được
#balance thuộc về instance nhưng không phải là key của property. JSON.stringify chỉ đọc các key của property.

Dù nhận được {}, số dư vẫn không mất: prepaidCard.balance vẫn đọc ra 3000. Khi lưu, hãy truyền một object được tạo từ giá trị của getter, như dòng cuối của ví dụ. Bảng dưới đây so sánh ba cách viết dùng trong bài.

Cách viếtTruy cập từ bên ngoàiKết quả JSON.stringify
Tên có _ (_balance)Đọc ghi được, không qua kiểm traCó, như {"_balance":3000}
Private field (#balance)SyntaxError lúc tải codeKhông có ({})
Private method (#withdraw)#withdraw(): SyntaxError / withdraw(): TypeErrorKhông có, vì là phương thức
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Điều gì xảy ra khi chạy code có dòng card.#balance = 0; bên ngoài class, nằm sau một console.log?

Câu 2Với card#withdraw, điều gì xảy ra nếu bạn gọi card.withdraw(500) từ bên ngoài?

Câu 3JSON.stringify cho ra gì với card#balance = 3000 và chỉ có getter?