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?
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 field và private 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.balance và this.#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
#balance = 0— tên chỉ dùng được bên trong thân này
- Nếu
amount > 20000thìthrow - Chỉ số tiền hợp lệ mới chạy
this.#balance += amount
return this.#balance— chỉ trả về giá trị, không bao giờ thay đổi nó
card.charge(5000)— thay đổi số dư thông qua chargecard.balance— đọc được 8000 thông qua getter- Không thể viết tên
#balanceở đây
#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 đó.
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);
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.
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)
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).
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}
- Không có key nào (
get balance()là phương thức viết trên class) JSON.stringify(prepaidCard)cho ra{}
#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ết | Truy cập từ bên ngoài | Kết quả JSON.stringify |
|---|---|---|
| Tên có _ (_balance) | Đọc ghi được, không qua kiểm tra | Có, như {"_balance":3000} |
| Private field (#balance) | SyntaxError lúc tải code | Không có ({}) |
| Private method (#withdraw) | #withdraw(): SyntaxError / withdraw(): TypeError | Không có, vì là phương thức |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Với card có #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 có #balance = 3000 và chỉ có getter?