Câu 1draft là instance của class không có phương thức nào trong 3 phương thức trên. Chạy const [firstLine] = draft; thì điều gì xảy ra?
Phương thức đặc biệt — toString, toJSON và Symbol.iterator
toString, toJSON và Symbol.iterator là các phương thức JavaScript tự gọi. Bài viết gồm cách quyết định dạng hiển thị, chọn giá trị đưa vào JSON và duyệt class bằng for...of.
Đưa một object đơn hàng vào template literal thì hiện ra [object Object], dùng JSON.stringify thì mất tổng tiền tính bằng getter, còn đưa vào for...of thì báo lỗi TypeError.
Bài này nói về 3 phương thức đặc biệt (phương thức mà JavaScript tìm theo tên và tự gọi trong những tình huống nhất định): phương thức được gọi khi hiển thị, khi chuyển sang JSON và khi lặp.
Quyết định cách hiển thị khi đưa vào chuỗi — toString
Giả sử bạn đưa đơn hàng vào một thông báo bằng cách viết Đã nhận: ${order}. Kết quả hiện ra không phải mã đơn hay số tiền mà là [object Object]. Nếu màn hình nào cũng tự ghép lại từ order.id và order.total, đoạn code ghép chuỗi giống nhau sẽ xuất hiện khắp nơi.
toString (phương thức mà JavaScript gọi khi cần biến một giá trị thành chuỗi, và dùng giá trị trả về làm chuỗi đó) được gọi bởi ${}, String() và join của mảng. Nếu class không định nghĩa, toString mặc định ở Object.prototype, nằm xa hơn trên chuỗi prototype, sẽ trả về [object Object].
// Class không có toString
class DraftOrder {
constructor(id, total) { this.id = id; this.total = total; }
}
// Class có toString
class Order {
constructor(id, total) { this.id = id; this.total = total; }
toString() { return `Đơn ${this.id} (${this.total} yên)`; }
}
const draft = new DraftOrder("A-101", 3200);
const order = new Order("A-102", 4800);
// Đưa vào template literal thì toString được gọi
console.log(`Đã nhận: ${draft}`); // Đã nhận: [object Object]
console.log(`Đã nhận: ${order}`); // Đã nhận: Đơn A-102 (4800 yên)
// String() và join của mảng cũng dùng cùng toString đó
console.log(String(order)); // Đơn A-102 (4800 yên)
console.log([order, new Order("A-103", 1500)].join(" / ")); // Đơn A-102 (4800 yên) / Đơn A-103 (1500 yên)
Ở cả hai hàng, phía gọi đều là ${}. Muốn đổi dạng hiển thị, bạn chỉ cần sửa toString của class ở một chỗ, và mọi dòng nhúng object đó đều thay đổi theo. Khi truyền thẳng instance như console.log(order), toString không được gọi.
Đưa tổng tiền vào JSON cần lưu — toJSON
Để gửi đơn hàng lên server, bạn chuyển nó thành chuỗi bằng JSON.stringify(order). Với class giữ các dòng hàng trong #lines và tính tổng bằng getter total, cả hai đều không phải property thường của instance, nên dữ liệu gửi đi chỉ là {"id":"A-102"}.
Nếu viết toJSON (phương thức mà JSON.stringify gọi trước khi chuyển một giá trị, rồi chuyển giá trị trả về của nó thay cho giá trị gốc), bạn có thể trả về một object chứa đúng những giá trị muốn gửi. Date trở thành chuỗi ngày giờ trong JSON cũng là vì Date có phương thức này.
class Order {
#lines; // Các dòng hàng (không đọc được từ bên ngoài)
constructor(id, lines) { this.id = id; this.#lines = lines; }
get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
// Được gọi khi JSON.stringify, giá trị trả về mới là thứ được chuyển thành chuỗi
toJSON() { return { id: this.id, itemCount: this.#lines.length, total: this.total }; }
}
const order = new Order("A-102", [{ price: 1200, quantity: 2 }, { price: 2400, quantity: 1 }]);
const other = new Order("A-103", [{ price: 1500, quantity: 1 }]);
// Key riêng của instance chỉ có id
console.log(Object.keys(order).join(", ")); // id
// Giá trị trả về của toJSON được chuyển thành chuỗi
console.log(JSON.stringify(order)); // {"id":"A-102","itemCount":2,"total":4800}
// Khi nằm trong mảng, toJSON được gọi cho từng phần tử
console.log(JSON.stringify([order, other]));
// [{"id":"A-102","itemCount":2,"total":4800},{"id":"A-103","itemCount":1,"total":1500}]
#lines vẫn không đọc được từ bên ngoài; chính class quyết định giá trị nào được đưa vào JSON. Ngay cả giá trị mà instance không giữ dưới dạng property, như itemCount, cũng xuất hiện trong JSON nếu bạn thêm nó vào object trả về. Khi dùng JSON.parse để đọc ngược lại, bạn nhận được một object thường có dạng này, chứ không phải instance của Order.
toJSON trả về chuỗi thì bị chuyển hai lần
Nếu trả về một chuỗi như return JSON.stringify({ id: this.id, total: this.total });, chuỗi đó sẽ bị chuyển sang JSON thêm một lần nữa. Dù đã JSON.parse một lần, kết quả vẫn là chuỗi, và .total là undefined. Hãy trả về một object.
Duyệt các dòng hàng bằng for...of — Symbol.iterator
Tiếp theo, bạn muốn hiển thị lần lượt từng dòng hàng của đơn lên màn hình. Các dòng hàng được giấu trong #lines nên không đọc được từ bên ngoài, và viết for (const line of order) sẽ báo lỗi TypeError: order is not iterable (không lặp được). Kể cả khi giữ các dòng hàng trong một mảng public, bạn vẫn không lặp trực tiếp trên instance được.
Nếu bạn định nghĩa một phương thức có tên là Symbol.iterator (một giá trị có sẵn, không trùng với bất kỳ key nào khác, dùng làm tên của phương thức mà for...of gọi), for...of sẽ nhận từng giá trị một bằng cách gọi next() trên object mà phương thức đó trả về.
Trong class, generator function* tên() được viết là *tên(). Khi viết tên trong ngoặc vuông như [Symbol.iterator], giá trị trong ngoặc sẽ được dùng làm tên phương thức.
class Order {
#lines; // Các dòng hàng (không đọc được từ bên ngoài)
constructor(id, lines) { this.id = id; this.#lines = lines; }
// Phương thức mà for...of gọi. Mỗi dòng hàng được yield sẽ vào biến của vòng lặp
*[Symbol.iterator]() {
for (const line of this.#lines) yield line;
}
}
const order = new Order("A-102", [{ name: "Cà phê phin", quantity: 2 }, { name: "Cốc sứ", quantity: 1 }]);
// Nhận từng dòng hàng một từ bên ngoài class
for (const line of order) {
console.log(`${line.name} × ${line.quantity}`); // 2 dòng: Cà phê phin × 2 / Cốc sứ × 1
}
// Mỗi lần spread lại gọi phương thức, nên lần thứ hai cũng được 2 phần tử
console.log([...order].length, [...order].length); // 2 2
Generator đã đọc hết thì trở nên rỗng, như bài về iterator và generator đã nói. Phía gọi chỉ nhận được những dòng hàng được yield, không bao giờ nhận chính mảng #lines, nên không thể thêm hay xóa dòng hàng từ bên ngoài.
Trả về nguyên mảng thì for...of báo lỗi
Nếu trả về mảng như return this.#lines;, mảng không có next(), nên for...of báo lỗi TypeError: undefined is not a function. Mảng lặp được (iterable), nhưng bản thân nó không phải iterator. Hãy thêm * và yield từng phần tử.
Tìm phương thức bị thiếu từ kết quả — tổng hợp 3 phương thức
Khi dạng hiển thị hay JSON không ra như mong đợi, những dòng như ${order} hay JSON.stringify(order) không hề ghi tên phương thức mà chúng gọi. Muốn biết mình quên viết phương thức nào, bạn phải lần ngược từ cách gọi và dạng của kết quả.
Ngay cả với instance của class không có phương thức nào trong 3 phương thức trên, mỗi cách gọi vẫn tìm một cái tên cố định của riêng nó dọc theo chuỗi prototype. Chỉ có toString mặc định ở Object.prototype là được tìm thấy, còn toJSON và [Symbol.iterator] thì tìm tới tận null cũng không thấy.
// Class không có toString, toJSON hay [Symbol.iterator]
class DraftOrder {
#lines;
constructor(id, lines) { this.id = id; this.#lines = lines; }
get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
}
const draft = new DraftOrder("A-101", [{ name: "Cà phê phin", price: 1200, quantity: 2 }]);
// toString được tìm thấy ở Object.prototype; không có toJSON nên chỉ liệt kê key riêng
console.log(`${draft}`); // [object Object]
console.log(JSON.stringify(draft)); // {"id":"A-101"}
// [Symbol.iterator] không có ở đâu cả, nên báo lỗi
try {
console.log([...draft]);
} catch (error) {
console.log(error.message); // draft is not iterable
}
JSON.stringify không báo lỗi mà trả về {"id":"A-101"}, nên dữ liệu thiếu các dòng hàng trong #lines và total vẫn được gửi đi, và chỉ đến khi phía nhận xử lý mới có người phát hiện. Bảng dưới đây liệt kê những cách gọi khác cũng tìm cùng các tên này, và kết quả khi thiếu phương thức.
| Cách gọi | Phương thức được tìm | Kết quả khi thiếu |
|---|---|---|
| JSON.stringify([draft]) | toJSON (từng phần tử) | [{"id":"A-101"}] |
| for (const line of draft) | [Symbol.iterator] | TypeError: draft is not iterable |
| const [firstLine] = draft | [Symbol.iterator] | TypeError: draft is not iterable |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Có một mảng gồm 2 đơn hàng, mỗi đơn có toJSON trả về { id: this.id }. Dùng JSON.stringify cho mảng này thì kết quả thế nào?
Câu 3Với cart có *[Symbol.iterator](), nếu viết [...cart] hai lần thì mảng thứ hai dài bao nhiêu?