Câu 1Bạn thêm PdfFile vào một danh sách có vòng lặp gọi file.preview(). Bạn cần viết thêm ở đâu?
Polymorphism — cùng một lời gọi, cách chạy khác nhau
Viết cho mỗi class một phương thức cùng tên để một lời gọi dùng được cho tất cả. Gồm mảng lẫn nhiều loại, kiểm tra instanceof và duck typing.
Giả sử form liên hệ có danh sách file đính kèm: ảnh thì hiện ảnh thu nhỏ, video thì hiện nút phát. Nếu bạn lưu loại file dưới dạng chuỗi và dùng if trong hàm hiển thị để rẽ nhánh, thì mỗi lần thêm một loại, bạn lại phải mở hàm đó ra để viết thêm.
Bài này nói về polymorphism (đa hình), khi một phương thức cùng tên chạy code khác nhau ở mỗi class, và duck typing, cách làm không quan tâm đến quan hệ kế thừa.
Đặt phần hiển thị khác nhau vào class — phương thức cùng tên
Trong cách làm cũ, mỗi file đính kèm là một object như { kind: "video", fileName: "demo.mp4" }, và hàm previewOf rẽ nhánh theo kind. Để thêm video, bạn phải mở previewOf, nơi cũng đang chứa nhánh xử lý ảnh, và viết thêm vào đó.
Với polymorphism (các class khác nhau có một phương thức cùng tên; phía gọi chỉ viết một lời gọi, còn code nào chạy là do class đã tạo ra instance quyết định), bạn viết phần hiển thị của từng loại vào preview của lớp con.
Khi mỗi lớp con đã override preview của lớp cha, phía gọi chỉ cần viết preview() mà không phải kiểm tra đó là lớp con nào.
// Lưu loại file dưới dạng chuỗi và rẽ nhánh bằng if trong hàm hiển thị
function previewOf(file) {
if (file.kind === "image") return `${file.fileName}: Hiện ảnh thu nhỏ`;
return `${file.fileName}: Không có bản xem trước`;
}
console.log(previewOf({ kind: "video", fileName: "demo.mp4" })); // demo.mp4: Không có bản xem trước
// Viết phương thức preview cùng tên trong class của từng loại
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: Không có bản xem trước`; }
}
class ImageFile extends Attachment {
preview() { return `${this.fileName}: Hiện ảnh thu nhỏ`; }
}
class VideoFile extends Attachment {
preview() { return `${this.fileName}: Hiện nút phát`; }
}
console.log(new ImageFile("logo.png").preview()); // logo.png: Hiện ảnh thu nhỏ
console.log(new VideoFile("demo.mp4").preview()); // demo.mp4: Hiện nút phát
console.log đầu tiên in ra "Không có bản xem trước" vì previewOf chưa có nhánh cho "video". Với cách dùng class, người thêm video chỉ cần dùng đúng tên preview, và không cần nhớ những chuỗi nào được đặt vào kind.
Gọi một lượt cho nhiều loại lẫn nhau — mảng và for...of
Danh sách được gửi tới dưới dạng một mảng duy nhất, lẫn cả ảnh, PDF và những file không xác định được loại. Nếu gán từng file vào một biến riêng rồi viết lời gọi preview() cho từng biến, thì mỗi khi số file đính kèm thay đổi, bạn lại phải thêm hoặc bớt dòng gọi.
Instance của các class khác nhau vẫn có thể nằm chung một mảng. Khi duyệt bằng for...of, lời gọi file.preview() trong thân vòng lặp chạy trên instance của một class khác ở mỗi lượt, và dòng gọi vẫn chỉ là một dòng.
Khi thêm PdfFile cho file PDF, bạn cũng không phải sửa vòng lặp.
// Attachment và ImageFile được định nghĩa như ở ví dụ đầu tiên
// Loại được thêm sau. Vòng lặp không cần sửa
class PdfFile extends Attachment {
preview() { return `${this.fileName}: Hiện trang 1`; }
}
const attachments = [
new ImageFile("logo.png"),
new PdfFile("manual.pdf"),
new Attachment("memo.txt"),
];
// Thân vòng lặp chỉ một dòng. Ở mỗi lượt, file chứa instance của một class khác
for (const file of attachments) {
console.log(file.preview());
}
// logo.png: Hiện ảnh thu nhỏ
// manual.pdf: Hiện trang 1
// memo.txt: Không có bản xem trước
memo.txt ở lượt thứ ba được tạo từ Attachment, nên preview viết trong chính Attachment được chạy. Nếu một lớp con mới quên viết preview, nó cũng sẽ ra đúng kết quả này thay vì báo lỗi, vì vậy mỗi khi thêm một loại, hãy kiểm tra dòng của loại đó trong danh sách.
Gọi phương thức mà chỉ một loại có — rẽ nhánh bằng instanceof
Tiếp theo, các dòng ảnh trong danh sách cần thêm nút cắt ảnh. cropButton, phương thức trả về nút đó, chỉ được viết trong ImageFile, nên nếu gọi file.cropButton() cho mọi phần tử trong vòng lặp, chương trình sẽ dừng với TypeError ở lượt của memo.txt.
instanceof trả về true nếu giá trị bên trái được tạo từ class bên phải hoặc từ một lớp con của nó. Hãy kiểm tra file instanceof ImageFile và chỉ gọi cropButton ở những lượt cho ra true.
Class nào cũng có preview, nên bạn gọi thẳng mà không cần kiểm tra.
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: Không có bản xem trước`; }
}
class ImageFile extends Attachment {
preview() { return `${this.fileName}: Hiện ảnh thu nhỏ`; }
cropButton() { return `${this.fileName}: Hiện nút cắt ảnh`; } // Chỉ ImageFile có
}
const attachments = [new ImageFile("logo.png"), new Attachment("memo.txt")];
for (const file of attachments) {
console.log(file.preview()); // Class nào cũng có, nên gọi mà không cần kiểm tra
if (file instanceof ImageFile) {
console.log(file.cropButton()); // Chỉ gọi ở lượt của ImageFile
}
}
// logo.png: Hiện ảnh thu nhỏ
// logo.png: Hiện nút cắt ảnh
// memo.txt: Không có bản xem trước
Hàng trên báo lỗi TypeError: file.cropButton is not a function. Nếu dùng instanceof để rẽ nhánh cả với preview, bạn sẽ phải thêm một lần kiểm tra cho mỗi loại, giống hệt previewOf. Chỉ dùng nhánh instanceof cho những phương thức mà chỉ một số class có.
Bọc instanceof trong ngoặc khi dùng !
Trong !file instanceof ImageFile, !file được tính trước và thành false, nên thực chất bạn đang kiểm tra false instanceof ImageFile, và kết quả luôn là false. Muốn viết "không phải ImageFile", hãy bọc trong ngoặc: !(file instanceof ImageFile).
Đưa vào danh sách cả giá trị không kế thừa — duck typing
Danh sách còn cần hiển thị cả liên kết tới thư mục. Liên kết là một object literal có phương thức preview(), và không kế thừa Attachment. Nếu bạn kiểm tra instanceof Attachment để loại bỏ dữ liệu cũ không có preview, như { fileName: "old.doc" }, thì liên kết cũng ra false và bị xếp chung với dữ liệu cũ vào nhóm "Không hiển thị được".
Với duck typing (quyết định cách xử lý một giá trị chỉ dựa vào việc nó có những phương thức cần thiết hay không, không kiểm tra class hay quan hệ kế thừa), bạn kiểm tra typeof item.preview === "function".
Một lời gọi phương thức sẽ chạy dù class là gì, miễn là giá trị bên trái dấu chấm có một hàm mang tên đó.
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: Không có bản xem trước`; }
}
// Liên kết tạo mà không dùng class. Không kế thừa Attachment
const folderLink = {
fileName: "Thư mục báo giá",
preview() { return `${this.fileName}: Mở liên kết`; },
};
console.log(folderLink instanceof Attachment); // false
// Quyết định dựa vào việc có preview là hàm hay không, không dựa vào class
for (const item of [new Attachment("memo.txt"), folderLink, { fileName: "old.doc" }]) {
if (typeof item.preview === "function") {
console.log(item.preview()); // memo.txt: Không có bản xem trước / Thư mục báo giá: Mở liên kết
} else {
console.log(`${item.fileName}: Không hiển thị được`); // old.doc: Không hiển thị được
}
}
Vì không có bước kiểm tra instanceof, một giá trị có hàm preview không trả về chuỗi cũng lọt qua. Hãy đảm bảo mọi giá trị đưa vào danh sách đều có preview() trả về chuỗi để hiển thị. Bảng dưới đây so sánh ba cách gọi dùng trong bài.
| Cách gọi | Kiểm tra gì | Khi nào dùng |
|---|---|---|
| Gọi thẳng preview() | Không kiểm tra gì | Mọi phần tử đều có preview |
| Rẽ nhánh bằng instanceof | Tạo từ ImageFile hay lớp con? | Gọi phương thức chỉ class đó có |
| Rẽ nhánh bằng typeof | preview có phải là hàm? | Nhận mọi giá trị có preview |
Toán tử in trả về true dù preview không phải là hàm
"preview" in item vẫn là true khi giá trị là chuỗi, như trong { preview: "Sắp ra mắt" }. Gọi item.preview() trên giá trị đó sẽ báo lỗi TypeError: item.preview is not a function, nên trước khi gọi, hãy dùng typeof để chắc chắn đó thật sự là một hàm.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Với memo được tạo từ Attachment, kết quả của !memo instanceof ImageFile là gì?
Câu 3Với object literal link có phương thức preview(), link instanceof Attachment và typeof link.preview cho ra gì?