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

constructor và class field — cách đặt giá trị ban đầu

constructor và class field đặt giá trị ban đầu cho instance: nhận đối số, thứ tự thêm field, giá trị mặc định và chặn giá trị không hợp lệ.

Một ticket hỗ trợ có những giá trị khác nhau ở mỗi ticket, như tiêu đề, và những giá trị ban đầu giống nhau ở mọi ticket, như trạng thái "Chưa xử lý". Nếu viết không cẩn thận, giá trị bị bỏ sót hoặc giá trị không hợp lệ sẽ lọt thẳng vào instance.

Bài này nói về constructor, nơi nhận các giá trị lúc tạo instance, và class field, nơi khai báo các giá trị ban đầu cố định.

Chuẩn hóa đối số trước khi lưu — phần thân constructor

Tiêu đề ticket gửi từ form hỗ trợ đôi khi có khoảng trắng thừa ở đầu hoặc cuối. Nếu bạn ghép chuỗi hiển thị như "Minh: Không đăng nhập được" sau khi đã tạo instance, thì màn hình nào hiển thị nó cũng phải tự viết trim và đoạn ghép chuỗi.

Trong phần thân constructor, vế phải của this.tên = không nhất thiết phải là đối số nguyên dạng: bạn có thể lưu giá trị đã được chuẩn hóa, như this.title = title.trim(), hoặc giá trị ghép từ nhiều đối số. Khi đó, màn hình nào cũng chỉ cần đọc property của instance là có giá trị đã gọn gàng.

class Ticket {
  constructor(title, customerName) {
    // Chuẩn hóa giá trị nhận vào rồi mới lưu vào this
    this.title = title.trim();
    this.customerName = customerName;

    // Giá trị để hiển thị được ghép từ this sau khi đã chuẩn hóa
    this.label = `${this.customerName}: ${this.title}`;
    // Nếu viết `${customerName}: ${title}`, khoảng trắng trong tiêu đề sẽ còn nguyên
  }
}

// Truyền tiêu đề có khoảng trắng ở hai đầu
const ticket = new Ticket("  Không đăng nhập được ", "Minh");
console.log(ticket.title);    // Không đăng nhập được
console.log(ticket.label);    // Minh: Không đăng nhập được
label được ghép từ giá trị nào
Ghép từđối số titletitle vẫn cònkhoảng trắngChèn cảkhoảng trắngThừa khoảng trắngsau "Minh:"Ghép từthis.titlethis.titleđã được trimChèn tiêu đềđã chuẩn hóaMinh: Khôngđăng nhập được
Cả hai hàng đều không báo lỗi, khác biệt chỉ hiện ra ở phần hiển thị. Muốn dùng giá trị đã chuẩn hóa, hãy đọc lại từ this sau khi đã lưu.

Hàng trên đọc đối số title, còn hàng dưới đọc this.title sau khi trim. Nếu bạn chuyển dòng label lên đầu constructor, lúc đó cả this.title lẫn this.customerName đều chưa được gán, nên label sẽ thành undefined: undefined.

Hãy tạo dữ liệu đặt phòng từ các giá trị trong form đặt chỗ. selectedRoom, selectedStart và selectedHours đã được khai báo sẵn.

① Hãy định nghĩa Reservation để mỗi instance giữ tên phòng, giờ bắt đầu và thời lượng sử dụng.

② Trong constructor ở ①, hãy lưu thêm giờ kết thúc dưới tên endHour.

③ Hãy truyền 3 giá trị đó vào để tạo booking.

④ Hãy hiển thị theo dạng "Phòng: X:00-Y:00 (Z giờ)".

(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

Liệt kê các giá trị ban đầu cố định — class field

Ticket nào mới tiếp nhận cũng bắt đầu với trạng thái "Chưa xử lý" và 0 bình luận. Nếu bạn đặt những giá trị ban đầu không liên quan tới đối số như vậy trong constructor bằng this.status = "Chưa xử lý";, chúng sẽ lẫn với các dòng lưu đối số, và muốn biết instance có những giá trị gì thì phải đọc hết phần thân.

Class field (property được viết trong cặp ngoặc nhọn của class nhưng bên ngoài constructor và các phương thức, được thêm vào instance mỗi lần new) chỉ gồm tên và giá trị ban đầu, không có this, như status = "Chưa xử lý";. Khi đọc nó từ phần thân constructor hay từ phương thức, bạn viết this.status giống như mọi property khác.

class Ticket {
  // Giá trị ban đầu không phụ thuộc đối số thì đặt thành field
  status = "Chưa xử lý";
  comments = [];

  constructor(title) {
    this.title = title;
    this.summary = `[${this.status}] ${title}`;   // Đọc được giá trị của field
  }
}

const first = new Ticket("Không đăng nhập được");
const second = new Ticket("Chưa nhận được hóa đơn");
console.log(first.summary);                // [Chưa xử lý] Không đăng nhập được

// comments là các mảng riêng biệt, được tạo mỗi lần new
first.comments.push("Đã nhờ khách khởi động lại");
console.log(first.comments.length);        // 1
console.log(second.comments.length);       // 0
Thứ tự các giá trị được thêm vào trong new Ticket
new Ticket("Khôngđăng nhập được")① Thêm các fieldtừ trên xuống② Chạy phần thân,đọc this.status③ Gán instancecho firstInstanceđang trốngstatus:"Chưa xử lý"comments: []Thêm titlevà summaryInstance có4 property
Bên trong new, các field được thêm vào trước, sau đó phần thân constructor mới chạy. Trong phần thân, bạn đã đọc được giá trị của field.

Bước ① được thực hiện lại mỗi lần new, nên comments = [] cũng là một mảng riêng cho từng instance, và bình luận thêm vào first không xuất hiện trong second. Đó là lý do second.comments.length vẫn là 0.

Viết this. cho field sẽ gây SyntaxError

Nếu bạn viết this.status = "Chưa xử lý"; ở vị trí dành cho field (bên ngoài constructor), bạn sẽ gặp SyntaxError: Unexpected token '.' và không dòng nào được chạy. Thêm let hay const vào cũng gây SyntaxError. Field chỉ gồm tên và giá trị ban đầu.

Hãy tạo dữ liệu giao hàng cho từng nơi nhận. destinations đã được khai báo sẵn.

① Hãy định nghĩa Shipment nhận nơi nhận, với giá trị ban đầu là phí vận chuyển 600 và trạng thái "Đã tiếp nhận".

② Trong constructor ở ①, hãy đổi phí thành 1500, chỉ khi nơi nhận là "Okinawa".

③ Hãy tạo Shipment cho từng nơi nhận và gom vào mảng shipments.

④ Hãy hiển thị từng đơn theo dạng "Nơi nhận: trạng thái / phí X yên".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Quyết định giá trị khi bỏ qua đối số — tham số mặc định

Hầu hết ticket có mức ưu tiên "Thường", và bạn chỉ muốn truyền "Gấp" khi có việc cần xử lý ngay. Nếu vừa viết field priority = "Thường"; vừa thêm this.priority = priority; vào phần thân, thì với ticket được tạo mà không truyền mức ưu tiên, priority lại không phải là "Thường".

Tham số mặc định (cách viết quy định giá trị được dùng khi không truyền đối số) mà bạn đã học ở bài về tham số mặc định và rest parameter cũng dùng được trong constructor dưới dạng priority = "Thường". Đối số bị bỏ qua hoặc là undefined sẽ được thay bằng giá trị mặc định trước khi phần thân chạy, nên this.priority = priority nhận được "Thường".

// Đặt giá trị mặc định trong field, và phần thân cũng gán đối số
class DraftTicket {
  priority = "Thường";
  constructor(title, priority) {
    this.title = title;
    this.priority = priority;   // priority bên phải là đối số (bỏ qua thì là undefined)
  }
}
console.log(new DraftTicket("Không đăng nhập được").priority);          // undefined

// Giá trị có thể đổi bằng đối số thì đặt mặc định bằng tham số mặc định
class Ticket {
  constructor(title, priority = "Thường") {
    this.title = title;
    this.priority = priority;
  }
}
console.log(new Ticket("Không đăng nhập được").priority);               // Thường
console.log(new Ticket("Chưa nhận được hóa đơn", "Gấp").priority);     // Gấp
Giá trị "Thường" của field bị ghi đè ra sao
new DraftTicket("Không đăngnhập được")① Thêmcác field② Phần thângán this.title③ Phần thângán this.priorityĐối số prioritylà undefinedthis.prioritylà "Thường"this.priority vẫnlà "Thường"this.prioritylà undefined
"Thường" của field được đặt trước phần thân, rồi phép gán trong phần thân ghi đè nó bằng undefined của đối số. Giá trị còn lại cuối cùng là giá trị được gán trong phần thân.

Giá trị có thể đổi bằng đối số thì viết thành tham số mặc định, còn giá trị không phụ thuộc đối số thì viết thành field. Với giá trị cần đổi tùy theo đối số, như phí vận chuyển ở Bài tập 2, hãy dùng if trong phần thân để đổi giá trị ban đầu của field. Bảng dưới đây so sánh giá trị cuối cùng của priority theo từng cách viết (kể cả cách không gán trong phần thân), khi chỉ truyền tiêu đề và khi truyền thêm "Gấp".

Cách viếtChỉ truyền tiêu đềTruyền thêm "Gấp"
Chỉ field (không gán trong thân)ThườngThường (bị bỏ qua)
Field + this.priority = priorityundefinedGấp
Tham số mặc định priority = "Thường"ThườngGấp

Không cho tạo với giá trị không hợp lệ — throw trong constructor

Nếu có ticket với tiêu đề trống, hoặc với mức ưu tiên không nằm trong các giá trị đã quy định, như "Khẩn", danh sách sẽ hiện ra những dòng trống, và ticket đó cũng bị bỏ sót khi đếm các ticket "Gấp". Kể cả khi bạn viết riêng một hàm kiểm tra sau khi tạo, màn hình nào quên gọi hàm đó vẫn dùng instance không hợp lệ.

Validation lúc tạo (kiểm tra xem giá trị nhận được có thỏa điều kiện hay không ngay tại thời điểm tạo instance) được viết bằng if trong constructor, và nếu không thỏa thì ném ra ngoại lệ bằng throw new Error(...). Khi ngoại lệ xảy ra, biểu thức new không trả về instance nào.

class Ticket {
  constructor(title, priority = "Thường") {
    // Chặn giá trị không hợp lệ bằng ngoại lệ trước khi lưu vào this
    if (title.trim() === "") {
      throw new Error("Tiêu đề đang trống");
    }
    if (priority !== "Thường" && priority !== "Gấp") {
      throw new Error(`Mức ưu tiên phải là Thường hoặc Gấp: ${priority}`);
    }
    this.title = title.trim();
    this.priority = priority;
  }
}

try {
  const ticket = new Ticket("Không đăng nhập được", "Khẩn");
  console.log(ticket.title);    // Dòng này không được chạy
} catch (error) {
  console.log(error.message);   // Mức ưu tiên phải là Thường hoặc Gấp: Khẩn
}
throw trong new đi tới đâu
Ngoặc của try (gọi new ở đây)
  • const ticket = new Ticket(...) — vế phải ném ra ngoại lệ, ticket không nhận được gì
  • console.log(ticket.title) — không được chạy
Thân constructor được gọi
  • Kiểm tra tiêu đề — không trống nên qua
  • Kiểm tra mức ưu tiên — là "Khẩn" nên throw
  • this.title = ... — từ đây trở xuống không được chạy
Ngoặc của catch (error)
  • error.message — "Mức ưu tiên phải là Thường hoặc Gấp: Khẩn"
Ngoại lệ thoát khỏi biểu thức new, bỏ qua phần còn lại của try và tới catch. Instance không hợp lệ không được gán cho biến nào cả.

Khi đặt phần kiểm tra trong constructor, dù có bao nhiêu chỗ viết new Ticket(...) thì cũng không ticket nào được tạo mà không qua kiểm tra. Nếu đưa giá trị nhận được vào thông báo của ngoại lệ, phía catch còn có thể hiển thị giá trị nào không hợp lệ.

Phép gán sau khi tạo không được kiểm tra

Phần kiểm tra trong constructor chỉ chạy lúc new. Nếu sau khi tạo bạn gán ticket.priority = "Khẩn";, sẽ không có lỗi nào và giá trị bị thay đổi luôn. Cách kiểm tra giá trị ở mỗi lần gán sẽ được giới thiệu trong bài về getter và setter.

Hãy chỉ nhận những dòng đơn hàng thỏa điều kiện. requests và acceptedLines đã được khai báo sẵn.

① Hãy định nghĩa OrderLine nhận tên sản phẩm và số lượng (nếu không có số lượng thì là 1).

② Hãy ném ra ngoại lệ "Tên sản phẩm đang trống" nếu tên sản phẩm trống, hoặc "Số lượng không hợp lệ: X" nếu số lượng không phải số nguyên từ 1 trở lên.

③ Hãy tạo OrderLine cho từng yêu cầu. Nếu tạo được, hãy thêm vào acceptedLines và hiển thị "Nhận: tên x số lượng"; nếu không, hãy hiển thị "Từ chối: thông báo của ngoại lệ".

④ Hãy hiển thị số dòng đã nhận theo dạng "Số dòng đã nhận: X".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

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

Câu 1Trong constructor của class có status = "Chưa xử lý";, đọc this.status thì nhận được gì?

Câu 2Class chỉ có field priority = "Thường"; và không gán trong phần thân. Nếu truyền "Gấp" thì sao?

Câu 3Trong try, constructor ném ra ngoại lệ ở const ticket = new Ticket(""). Điều gì xảy ra?