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

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ề gettersetter, 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
Lưu số chỗ trống hay tính mỗi lần đọc?
Lưu số chỗ trốngtrong constructorLưu 30 - 0lúc tạoGán 12cho reservedĐọc vẫnra 30Định nghĩa số chỗtrống bằng getKhông lưu gìlúc tạoGán 12cho reservedLúc đọc tính30 - 12 ra 18
Cả hai hàng đều thực hiện cùng một phép gán trước khi đọc. Getter tính toán với giá trị reserved ở đúng thời điểm đọc.

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.

Sau khi tăng giá, bảng thực đơn vẫn hiển thị giá gồm thuế cũ. Hãy sửa nó mà không thay đổi đoạn code đọc giá. MenuItem và menu đã được khai báo sẵn.

① Hãy thay priceWithTax (đang được tính trong constructor) bằng một property được tính mỗi lần đọc.

② Hãy tăng giá trước thuế của mọi món thêm 40 yên.

③ Hãy hiển thị "Tên món: X yên (gồm thuế)" cho mọi món.

④ Hãy hiển thị "Thuế: X yên" cho món đầu tiên.

(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

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 ifthrow 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
Phương thức nào chạy khi gán và khi đọc
get capacityset capacity(value)① seminar.capacity= 40② Đọcseminar.capacity③ seminar.capacity= 5set chạy:40 ≥ 12get chạy vàtrả về _capacityset chạy:5 nhỏ hơn 12_capacitythành 40Trả về 40Ném ngoại lệ;vẫn giữ 40
Khi property nằm bên trái =, 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.

Bạn đặt nhiệt độ cho máy lạnh theo các giá trị nhập từ bảng điều khiển. AirConditioner và temperatureInputs đã được khai báo sẵn.

① Hãy thêm property temperature để đọc và ghi nhiệt độ cài đặt, và ném ra ngoại lệ "Nhiệt độ phải từ 16 đến 30 độ: X" nếu giá trị nằm ngoài khoảng đó.

② Hãy lần lượt gán các giá trị nhập vào, hiển thị "Đã đặt X độ" nếu đặt được, hoặc thông báo của ngoại lệ nếu không.

③ Hãy hiển thị "Tên phòng: đang chạy ở X độ" với nhiệt độ cài đặt cuối cùng.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Đ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
}
Setter được gọi từ phép gán trong constructor
Thân của try (gọi new ở đây)
  • 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ề
Thân của constructor được gọi
  • this.reserved = 12 — lưu số người đăng ký trước khi kiểm tra
  • this.capacity = 5 — tên không có _ nên gọi set capacity
Thân của set capacity(value) được gọi
  • value là 5 và this.reserved là 12
  • 5 < 12 nên throw — không có gì được lưu vào _capacity
Dòng 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.reservedundefined. Trong phép so sánh số, undefined trở thành NaN, và 5 < NaNfalse, nên instance được tạo với sức chứa là 5.

Bạn đăng ký họ tên nhân viên, tách thành họ và tên riêng. staffNames đã được khai báo sẵn.

① Hãy định nghĩa Employee nhận họ tên đầy đủ và lưu họ, tên riêng biệt.

② Hãy thêm fullName để đọc và ghi theo dạng "Họ Tên", và ném ra ngoại lệ "Hãy tách họ và tên bằng dấu cách: X" nếu không có dấu cách.

③ Hãy tạo từng nhân viên một, và hiển thị nội dung ngoại lệ với người nào không tạo được.

④ Hãy gán "Phạm Minh" vào fullName của nhân viên đầu tiên, rồi hiển thị họ, tên và họ tên của mọi người.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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
Phép gán vào số chỗ trống được chấp nhận hay bị chặn?
Lưu số chỗ trốngthành một giá trịremainingSeats= 100100 được lưunguyên như vậyKhông khớp 30 chỗ,12 người đăng kýĐịnh nghĩa số chỗtrống chỉ bằng getremainingSeats= 100Không có set nênbáo lỗi TypeErrorVẫn là30 - 12 = 18
Ở hàng trên, phép gán đi qua và để lại một giá trị không khớp với sức chứa và số người đăng ký. Khi chỉ có getter, phép gán bị chặn ngay tại chỗ.

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ếtPhía gọi dùng thế nàoPhù hợp với
Property thường (reserved)seminar.reserved = 20 lưu nguyênGiá trị không cần kiểm tra
Chỉ getter (remainingSeats)Chỉ đọc: seminar.remainingSeatsGiá trị tính từ giá trị khác
Getter và setter (capacity)seminar.capacity = 40 được kiểm traGiá 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ố
QUIZ

Kiểm tra kiến thức

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

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ì?

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?