Câu 1Nếu constructor của EBook gán this.fileSize trước khi gọi super(), điều gì xảy ra khi new?
Kế thừa — extends và super
Tạo class mới dựa trên một class có sẵn bằng extends. Gồm super() trong constructor của lớp con, override và super.tênPhươngThức().
Giả sử một trang bán sách online cần thêm class cho sách điện tử, bên cạnh class Book dành cho sách in. Khác biệt chỉ là sách điện tử tải xuống được và không mất phí vận chuyển, vậy mà với cách viết hiện tại, bạn lại phải viết lại cả phần xử lý tên sách và giá.
Bài này nói về extends để xây class mới dựa trên một class có sẵn, và super để gọi code của class gốc đó.
Dùng lại code mà không phải chép — extends
Sách điện tử cũng cần một constructor nhận tên sách và giá, cùng phương thức describe trả về "tên sách: giá yên". Nếu chép phần thân của Book sang một class sách điện tử mới, những dòng giống hệt nhau sẽ nằm ở hai nơi. Khi đổi cách hiển thị mà quên sửa một bản, mỗi màn hình sẽ hiển thị theo một kiểu khác nhau.
Kế thừa (cơ chế cho phép một class khác dùng constructor và phương thức của class có sẵn) được viết là class EBook extends Book. Book được gọi là lớp cha, còn EBook là lớp con.
extends Error mà bạn đã viết trong bài về throw và lỗi tùy chỉnh cũng chính là kế thừa.
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title}: ${this.price} yên`; }
}
// Dựa trên Book và thêm phương thức chỉ sách điện tử mới có
class EBook extends Book {
download() { return `Đang tải xuống ${this.title}`; }
}
const ebook = new EBook("Thống kê nhập môn", 2200); // Các đối số được truyền cho constructor của Book
console.log(ebook.describe()); // Thống kê nhập môn: 2200 yên
console.log(ebook.download()); // Đang tải xuống Thống kê nhập môn
const book = new Book("Thống kê nhập môn", 2640);
console.log(typeof book.download); // undefined (lớp cha không có phương thức của lớp con)
Bên trong ebook.describe(), this là ebook, nên this.title viết trong Book sẽ đọc tên sách mà bạn đã truyền cho new EBook. Còn việc gán tên sách vào ebook là do constructor của Book đảm nhận khi nhận các đối số đó.
Thêm giá trị cho lớp con — super() và constructor
Giờ giả sử sách điện tử cần lưu thêm dung lượng file fileSize, ngoài tên sách và giá. Để nhận dung lượng, EBook phải có constructor riêng. Nhưng ngay khi bạn viết nó, constructor của Book không còn được gọi tự động nữa, và nếu cứ thế gọi new thì sẽ gặp ReferenceError.
super() (cách gọi constructor của lớp cha từ bên trong constructor của lớp con) nhận các đối số cần truyền cho lớp cha, như super(title, price);.
Trong lớp con, this chỉ có sau khi constructor của lớp cha chạy xong bên trong super(), nên hãy gọi super() trước mọi dòng dùng this.
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
}
class EBook extends Book {
constructor(title, price, fileSize) {
// Nếu viết this.fileSize = fileSize; phía trên dòng này, new sẽ báo lỗi ReferenceError
super(title, price); // constructor của Book gán title và price
this.fileSize = fileSize; // Sau đó mới thêm giá trị chỉ EBook có
}
}
const ebook = new EBook("Thống kê nhập môn", 2200, 12);
console.log(ebook.title); // Thống kê nhập môn
console.log(ebook.fileSize); // 12
console.log(Object.keys(ebook).join(", ")); // title, price, fileSize
Các key xuất hiện theo thứ tự title, price, fileSize vì Book đã gán giá trị của nó trước, bên trong super(). Hàng trên sẽ báo lỗi ReferenceError: Must call super constructor in derived class before accessing 'this' …, trong đó derived class nghĩa là lớp con.
Lớp con không đọc được field # của lớp cha
Nếu Book có private field #price, super() vẫn gán giá trị cho nó, nhưng viết this.#price trong phần thân của EBook sẽ gây SyntaxError. Tên bắt đầu bằng # chỉ dùng được bên trong class khai báo nó, nên lớp con phải đọc qua getter của lớp cha.
Đổi cách tính phí vận chuyển trong lớp con — override
Nếu thêm phương thức shippingFee trả về phí vận chuyển vào Book, sách điện tử cũng sẽ có phí vận chuyển giống sách in. Bạn có thể dùng if trong Book để phân biệt loại sách và trả về 0 yên riêng cho sách điện tử, nhưng khi đó mỗi lần thêm một lớp con, lớp cha lại phải thêm một nhánh.
Override (ghi đè: viết trong lớp con một phương thức trùng tên với phương thức của lớp cha, để instance của lớp con chạy phiên bản của lớp con) chỉ đơn giản là viết lại shippingFee trong phần thân của EBook. Khi bạn gọi một phương thức, JavaScript tìm tên đó bắt đầu từ class đã tạo ra instance, và nếu không có thì tìm tiếp ở lớp cha.
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title}: ${this.price} yên`; }
shippingFee() { return this.price >= 3000 ? 0 : 500; } // Miễn phí vận chuyển từ 3000 yên trở lên
}
class EBook extends Book {
shippingFee() { return 0; } // Viết lại với cùng tên (không có gì để giao)
}
const book = new Book("Thống kê nhập môn", 2640);
const ebook = new EBook("Thống kê nhập môn", 2200);
console.log(book.shippingFee()); // 500
console.log(ebook.shippingFee()); // 0 (shippingFee của EBook được chạy)
console.log(ebook.describe()); // Thống kê nhập môn: 2200 yên (describe của Book được chạy)
shippingFee của EBook không dùng cách tính của Book, nên dù bạn đổi điều kiện phí vận chuyển của Book, sách điện tử vẫn là 0 yên. Cách gọi code của lớp cha rồi chỉ viết thêm phần khác biệt sẽ được trình bày ở phần tiếp theo.
Viết thêm vào code của lớp cha — super.tênPhươngThức()
Tiếp theo, giả sử bạn muốn phần mô tả của sách điện tử có thêm "(sách điện tử, 12 MB)" ở cuối. Nếu override describe trong EBook và chép luôn đoạn code tạo "tên sách: giá yên", thì lần tới khi đổi cách hiển thị của Book, riêng phần mô tả sách điện tử sẽ vẫn giữ kiểu cũ.
Với super.tênPhươngThức() (cách gọi phương thức viết trong lớp cha từ bên trong phương thức của lớp con), super.describe() trả về giá trị của describe trong Book. Lớp con chỉ cần thêm thông tin sách điện tử vào chuỗi đó.
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title}: ${this.price} yên`; }
}
class EBook extends Book {
constructor(title, price, fileSize) {
super(title, price);
this.fileSize = fileSize;
}
// Thêm thông tin sách điện tử vào giá trị trả về của describe trong Book
describe() { return `${super.describe()} (sách điện tử, ${this.fileSize} MB)`; }
}
const ebook = new EBook("Thống kê nhập môn", 2200, 12);
console.log(ebook.describe()); // Thống kê nhập môn: 2200 yên (sách điện tử, 12 MB)
super.describe() là một biểu thức cho ra giá trị trả về của describe trong Book, nên nếu muốn đặt thông tin sách điện tử lên trước, bạn chỉ cần đổi vị trí của nó trong template literal. Bảng dưới đây tóm tắt các cách viết kế thừa dùng trong bài và thứ được chạy ở mỗi cách.
| Cách viết | Viết ở đâu | Thứ được chạy |
|---|---|---|
| class EBook extends Book | Khai báo class | Constructor và phương thức của Book |
| super(title, price) | Constructor con, trước this | Constructor của Book |
| Phương thức trùng tên cha | Thân lớp con | Bản của EBook, với instance EBook |
| super.describe() | Trong phương thức con | describe của Book |
Thiếu super, phương thức sẽ tự gọi chính nó mãi
Nếu trong describe của lớp con bạn viết this.describe() thay cho super.describe(), phương thức sẽ tự gọi chính nó liên tục cho đến khi báo lỗi RangeError: Maximum call stack size exceeded. Muốn gọi code của lớp cha từ một phương thức đã override, hãy thêm super..
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Nếu cả Book và EBook đều có shippingFee, gọi ebook.shippingFee() thì phương thức nào chạy?
Câu 3Bên trong describe của EBook, làm sao lấy giá trị trả về của describe trong Book?