Câu 1Với một getter trả về 30 - this.reserved, nếu bạn đặt reserved là 20 rồi đọc getter thì nhận được gì?
Getter và setter — chạy code khi đọc hoặc gán property
Getter và setter chạy code khi property được đọc hoặc gán. Kèm computed property, kiểm tra giá trị được gán và property chỉ đọc.
Giả sử trang đăng ký hội thảo hiển thị số chỗ còn trống theo công thức "sức chứa − số người đăng ký". Nếu bạn tính số chỗ còn trống lúc tạo instance rồi lưu lại, con số hiển thị sẽ không đổi khi có thêm người đăng ký. Và nếu ai đó gán sức chứa nhỏ hơn số người đã đăng ký, cũng không có gì ngăn lại.
Bài này nói về getter và setter, hai loại phương thức chạy khi property được đọc hoặc được gán.
Tính lại mỗi lần đọc — get và computed property
Nếu bạn tính số chỗ còn trống bằng "sức chứa − số người đăng ký" trong constructor rồi lưu lại, thì dù sau đó bạn thay đổi số người đăng ký, số chỗ còn trống vẫn giữ giá trị lúc tạo. Chuyển nó thành phương thức thì tính lại được, nhưng khi đó phía gọi phải nhớ: sức chứa thì không có ngoặc, còn số chỗ trống thì phải thêm ngoặc.
Getter (phương thức viết trong class với get ở phía trước, chạy khi property được đọc) không có tham số, chẳng hạn get remainingSeats() { ... }; khi đọc property, bạn nhận được giá trị mà nó return.
Một property như remainingSeats, được tính từ các property khác rồi trả về, gọi là computed property (thuộc tính tính toán).
class Seminar {
reserved = 0; // Số người đăng ký
constructor(capacity) {
this.capacity = capacity; // Sức chứa
}
// Mỗi lần được đọc, tính từ sức chứa và số người đăng ký lúc đó
get remainingSeats() {
return this.capacity - this.reserved;
}
}
const seminar = new Seminar(30);
console.log(seminar.remainingSeats); // 30 (đọc mà không có ngoặc)
// Đọc lại sau khi có 12 người đăng ký
seminar.reserved = 12;
console.log(seminar.remainingSeats); // 18
Số 30 ở hàng trên được tính một lần lúc new rồi lưu lại. Getter ở hàng dưới không lưu gì cả và chạy biểu thức return mỗi lần bạn đọc seminar.remainingSeats, nên dù thay đổi reserved bao nhiêu lần, bạn cũng không cần thêm dòng nào để tính lại số chỗ trống.
Gọi getter kèm ngoặc sẽ báo lỗi TypeError
Getter được viết như một phương thức, nhưng phía gọi không thêm ngoặc. Nếu bạn viết seminar.remainingSeats(), bạn đang gọi giá trị 18 trả về như một hàm, và code báo lỗi TypeError: seminar.remainingSeats is not a function.
Kiểm tra giá trị được gán — set và validation
Khi hội thảo sắp diễn ra, địa điểm có thể cho phép tăng sức chứa từ 30 lên 40. Bạn có thể viết phần kiểm tra bằng if và throw trong constructor, nhưng nó chỉ chạy lúc new. Nếu sau đó bạn gán seminar.capacity = 5, nhỏ hơn 12 người đã đăng ký, sẽ không có gì ngăn lại và số chỗ trống hiển thị là -7.
Setter (phương thức viết trong class với set ở phía trước, chạy khi property được gán) nhận giá trị được gán qua tham số value. Giá trị thực được lưu dưới một tên khác, chẳng hạn _capacity, và getter cùng tên với property (get capacity) trả nó về.
Dấu _ ở đầu tên là một quy ước, nghĩa là "đừng truy cập trực tiếp từ bên ngoài".
class Seminar {
constructor(capacity, reserved) {
this.reserved = reserved; // Số người đăng ký
this._capacity = capacity; // Giá trị thực của sức chứa nằm ở _capacity
}
get capacity() { return this._capacity; }
set capacity(value) { // Giá trị được gán sẽ nằm trong value
if (value < this.reserved) {
throw new Error(`Sức chứa phải ít nhất bằng số người đăng ký ${this.reserved}: ${value}`);
}
this._capacity = value;
}
}
const seminar = new Seminar(30, 12);
seminar.capacity = 40; // set capacity chạy và lưu 40
console.log(seminar.capacity); // 40 (get capacity chạy)
seminar.capacity = 5; // Error: Sức chứa phải ít nhất bằng số người đăng ký 12: 5
=, set sẽ chạy; khi được đọc ở bất kỳ chỗ nào khác, get sẽ chạy. Giá trị không qua được phần kiểm tra sẽ không bao giờ tới _capacity.Dòng ③ ném ra ngoại lệ, nên nếu không bọc trong try, các dòng phía sau sẽ không chạy. Miễn là bạn gán qua capacity, giá trị luôn đi qua setter, nhưng nếu viết thẳng seminar._capacity = 5, nó sẽ bỏ qua phần kiểm tra. Bài tiếp theo sẽ nói về cách ngăn điều đó.
Gán vào cùng tên bên trong setter sẽ báo lỗi RangeError
Nếu bạn viết this.capacity = value; bên trong set capacity(value), phép gán đó lại gọi setter, setter lại gán tiếp, cứ thế cho đến khi báo lỗi RangeError: Maximum call stack size exceeded. Hãy lưu giá trị dưới một tên khác, chẳng hạn _capacity.
Đi qua setter cả lúc tạo — gán trong constructor
Trong Seminar ở trên, constructor lưu thẳng giá trị vào _capacity, nên new Seminar(5, 12) tạo ra instance mà không qua kiểm tra nào. Nếu bạn viết thêm cùng câu lệnh if đó trong constructor, mỗi lần đổi điều kiện bạn phải sửa hai chỗ, và nếu quên một chỗ, new và phép gán sẽ cho kết quả khác nhau.
Nếu bên trong constructor bạn gán vào tên không có _, như this.capacity = capacity, thì setter cũng chạy cho dòng đó. Phần kiểm tra chỉ nằm ở một chỗ là setter, và cả giá trị truyền cho new lẫn giá trị gán sau này đều được kiểm tra theo cùng một điều kiện.
class Seminar {
constructor(capacity, reserved) {
this.reserved = reserved; // Lưu trước số người đăng ký dùng để kiểm tra
this.capacity = capacity; // Tên không có _ nên set capacity chạy
}
get capacity() { return this._capacity; }
set capacity(value) {
if (value < this.reserved) {
throw new Error(`Sức chứa phải ít nhất bằng số người đăng ký ${this.reserved}: ${value}`);
}
this._capacity = value;
}
}
try {
new Seminar(5, 12); // new cũng đi qua phần kiểm tra của setter
} catch (error) {
console.log(error.message); // Sức chứa phải ít nhất bằng số người đăng ký 12: 5
}
new Seminar(5, 12)— gọi constructor- Ngoại lệ của setter quay ngược lên tới đây, và không có instance nào được trả về
this.reserved = 12— lưu số người đăng ký trước khi kiểm trathis.capacity = 5— tên không có_nên gọi set capacity
valuelà 5 vàthis.reservedlà 125 < 12nênthrow— không có gì được lưu vào_capacity
this.capacity = 5 gọi set capacity. Ngoại lệ thoát ra khỏi constructor và tới try.Nếu bạn chuyển this.reserved = reserved xuống dưới this.capacity = capacity, thì lúc setter chạy, this.reserved là undefined. Trong phép so sánh số, undefined trở thành NaN, và 5 < NaN là false, nên instance được tạo với sức chứa là 5.
Chặn phép gán — property chỉ có getter
Số chỗ còn trống phụ thuộc vào sức chứa và số người đăng ký. Nếu code của trang có thể viết seminar.remainingSeats = 100, trang sẽ hiển thị còn 100 chỗ trống cho sức chứa 30 với 12 người đăng ký, và ba con số không còn khớp nhau.
Property chỉ có getter mà không có setter cùng tên là property chỉ đọc (read-only: đọc được nhưng không gán được). Trong thân class và trong strict mode (chế độ biến một số lỗi thành ngoại lệ) mà môi trường chạy này sử dụng, dòng gán vào nó sẽ báo lỗi TypeError. Ngoài các trường hợp đó, phép gán bị bỏ qua mà không báo gì.
class Seminar {
constructor(capacity, reserved) {
this.capacity = capacity;
this.reserved = reserved;
}
get remainingSeats() { return this.capacity - this.reserved; } // Không có setter
}
const seminar = new Seminar(30, 12);
try {
seminar.remainingSeats = 100; // Gán vào tên không có set
} catch (error) {
console.log(error.name); // TypeError
}
console.log(seminar.remainingSeats); // 18 (giống như trước khi gán)
// Muốn đổi số chỗ trống, hãy đổi số người đăng ký mà nó dựa vào
seminar.reserved = 20;
console.log(seminar.remainingSeats); // 10
Hàng dưới báo lỗi vì tên chỉ có getter thì không có chỗ nào để đặt giá trị được gán, và cũng không có key mới nào được tạo trên instance. Bảng dưới đây liệt kê các cách viết dùng trong Seminar của bài này, cùng với phương thức để so sánh.
| Cách viết | Phía gọi dùng thế nào | Phù hợp với |
|---|---|---|
| Property thường (reserved) | seminar.reserved = 20 lưu nguyên | Giá trị không cần kiểm tra |
| Chỉ getter (remainingSeats) | Chỉ đọc: seminar.remainingSeats | Giá trị tính từ giá trị khác |
| Getter và setter (capacity) | seminar.capacity = 40 được kiểm tra | Giá trị cần kiểm tra khi gán |
| Phương thức (reserve(count)) | Gọi kèm ngoặc và đối số | Kết quả phụ thuộc đối số |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Nếu set capacity có dòng this.capacity = value; và bạn gán 40, điều gì xảy ra?
Câu 3Điều gì xảy ra nếu bạn gán 100 vào seminar.remainingSeats, property chỉ có getter?