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

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)
Cây kế thừa từ Book đến EBook
class Bookconstructor,describebook chỉ gọiđược describeclass EBookextends BookEBook chỉviết downloadInstance tạo bằngnew EBook(...)Gọi được cảdescribe lẫndownload
EBook chỉ viết download, nhưng vẫn gọi được describe. Chiều ngược lại thì không: book không gọi được download.

Bên trong ebook.describe(), thisebook, 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ố đó.

Hiển thị nhật ký mỗi lần quản trị viên đặt lại mật khẩu. Usertargets đã được khai báo sẵn.

① Hãy định nghĩa AdminUser kế thừa User.

② Hãy thêm resetPassword, trả về dòng nhật ký gồm phần mô tả của chính quản trị viên và tên của người dùng kia.

③ Hãy tạo một quản trị viên và hiển thị bản ghi cho tất cả mọi người trong targets.

④ Hãy hiển thị xem quản trị viên có đồng thời là instance của User hay không.

(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

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
super() có được gọi trước this không?
Viết this.fileSizetrướcsuper() chưađược gọiDừng ở dòngdùng thisReferenceErrorngay khi newViết super(title,price) trướcBook gántitle và priceGán 12 chothis.fileSizeebook cóđủ 3 giá trị
Ở hàng trên, dùng this trước super() sẽ gây lỗi và không có instance nào được tạo. Bạn chỉ dùng được this sau khi super() đã chạy xong.

Các key xuất hiện theo thứ tự title, price, fileSizeBook đã 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.

Liệt kê quyền của từng quản trị viên trên màn hình quản trị. UseradminInput đã được khai báo sẵn.

① Hãy định nghĩa AdminUser kế thừa User, nhận tên, email và một mảng quyền.

② Hãy gán tên và email trong constructor của User.

③ Hãy tạo quản trị viên từ adminInput và hiển thị tên cùng danh sách quyền.

④ Hãy hiển thị các key theo thứ tự, và xem status viết trong User nằm ở vị trí nào.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Đổ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)
JavaScript tìm phương thức được gọi ở đâu
EBook chỉ overrideshippingFeebook.shippingFee()ebook.shippingFee()ebook.describe()Tìm từ Book,thấy ở BookTìm từ EBook,thấy ở EBookEBook không có,thấy ở Book2640 dưới 3000nên ra 500Ra 0, khôngcần tìm tới BookThống kê nhập môn:2200 yên
Việc tìm kiếm bắt đầu từ class đã tạo ra instance. Nếu tìm thấy ở EBook, phương thức cùng tên của Book sẽ không 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.

Trên diễn đàn nội bộ, bạn quyết định có hiện nút sửa cho từng bài đăng hay không. Userposts đã được khai báo sẵn.

① Hãy định nghĩa AdminUser kế thừa User, và viết lại phần kiểm tra để quản trị viên sửa được mọi bài đăng.

② Hãy tạo người dùng thường "Linh" và quản trị viên "Minh".

③ Với từng bài đăng, hãy hiển thị hai người có sửa được hay không theo dạng "Bài N: Linh X / Minh Y".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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)
Thứ tự chạy bên trong ebook.describe()
Bắt đầu chạyebook.describe()this là ebook(fileSize là 12)super.describe()chạy describecủa Bookthis vẫn là ebook:Thống kê nhập môn:2200 yênThêm (sách điệntử, 12 MB) vàogiá trị trả vềTrả về Thống kênhập môn: 2200 yên(sách điện tử,12 MB)
describe của Book chạy giữa chừng trong describe của EBook rồi trả về. Phần ghép tên sách và giá chỉ nằm ở Book.

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ếtViết ở đâuThứ được chạy
class EBook extends BookKhai báo classConstructor và phương thức của Book
super(title, price)Constructor con, trước thisConstructor của Book
Phương thức trùng tên chaThân lớp conBản của EBook, với instance EBook
super.describe()Trong phương thức condescribe 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..

Trong một công cụ nội bộ, bạn áp dụng quy tắc mật khẩu chặt hơn chỉ cho quản trị viên. User, AdminUser, admin, memberpasswords đã được khai báo sẵn.

① Hãy thêm checkPassword vào AdminUser: chạy phần kiểm tra của User trước, và nếu qua thì kiểm tra thêm mật khẩu có từ 12 ký tự trở lên không.

② Hãy kiểm tra lần lượt các mật khẩu trong passwords với quản trị viên và hiển thị kết quả.

③ Hãy kiểm tra mật khẩu thứ hai với người dùng thường và hiển thị kết quả.

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 1Nếu constructor của EBook gán this.fileSize trước khi gọi super(), điều gì xảy ra khi new?

Câu 2Nếu cả BookEBook đề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?