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

Prototype — phía sau class

Prototype, nơi chứa phương thức của class, và prototype chain: Object.getPrototypeOf, hasOwnProperty so với in, và cách thêm phương thức từ bên ngoài.

Một khóa học tạo từ class gọi được summary(), nhưng bản thân instance lại không có phương thức đó. Nếu không biết phương thức nằm ở đâu, bạn cũng không giải thích được vì sao lớp con gọi được cả phương thức của lớp cha.

Bài này nói về prototype, nơi chứa các phương thức của class, và prototype chain (chuỗi prototype), thứ tự JavaScript lần theo khi tìm một property hay phương thức.

Gọi phương thức mà instance không có — prototype

Khi tạo 300 khóa học, có thể bạn sẽ thắc mắc liệu phần thân của summary có bị tạo ra 300 bản, mỗi instance một bản hay không. Object.keys(jsCourse) không liệt kê summary, nên nó không cho bạn biết các phương thức bạn gọi được lưu ở đâu.

Prototype (một object khác mà object hiện tại kế thừa property từ đó) có thể lấy ra bằng Object.getPrototypeOf(), và với jsCourse thì đó là Course.prototype. Những tên mà object không tự có sẽ được tìm ở đây.

Property prototype của class cũng chính là object này, nhưng jsCourse.prototype lại là undefined.

class Course {
  constructor(title, hours) {
    this.title = title;   // Tên khóa học
    this.hours = hours;   // Số giờ học
  }
  summary() { return `${this.title} (${this.hours} giờ)`; }
}
const jsCourse = new Course("JS cơ bản", 12);
const sqlCourse = new Course("SQL cơ bản", 8);

// summary không nằm trong các key của chính instance
console.log(Object.keys(jsCourse).join(", "));                       // title, hours

// Prototype của jsCourse là Course.prototype (không phải jsCourse.prototype)
console.log(Object.getPrototypeOf(jsCourse) === Course.prototype);   // true
console.log(jsCourse.prototype);                                     // undefined

// Cả hai khóa học dùng chung một summary trên Course.prototype
console.log(jsCourse.summary === sqlCourse.summary);                 // true
console.log(sqlCourse.summary());                                    // SQL cơ bản (8 giờ)
Class và instance trỏ tới đâu
new Course("JS cơ bản", 12)class Coursenew Course("SQL cơ bản", 8)jsCourseCourse.prototypecó summarysqlCourseKey của chính nó:title, hoursThân summarychỉ có mộtKhông tự có nêntìm ởCourse.prototypejsCourse.prototypelà undefinedHai summaryso sánh ra truesummary() cho raSQL cơ bản(8 giờ)
Prototype của cả hai khóa học chính là prototype của class. summary, thứ mà chúng không tự có, được tìm thấy ở đó.

Thân phương thức chỉ có một, nhưng this là giá trị nằm bên trái dấu chấm ở dòng gọi, nên sqlCourse.summary() tạo chuỗi từ các giá trị của sqlCourse. Dù có 300 khóa học, thứ tăng lên chỉ là các instance chứa titlehours.

Tìm lần lượt cho tới null — prototype chain

Nếu tạo khóa học phát trực tiếp bằng class LiveCourse extends Course, liveCourse.summary() cũng chạy được. Thế nhưng phương thức duy nhất được viết trong prototype của liveCourse, tức LiveCourse.prototype, chỉ là schedule.

Tên được tìm dọc theo prototype chain (chuỗi các tham chiếu có được khi lần theo prototype của prototype cho đến null), bắt đầu từ instance. extends đặt Course.prototype làm mắt xích kế tiếp sau LiveCourse.prototype, và sau đó là Object.prototype (object có sẵn nằm ở cuối chuỗi).

class Course {
  constructor(title, hours) { this.title = title; this.hours = hours; }
  summary() { return `${this.title} (${this.hours} giờ)`; }
}
class LiveCourse extends Course { schedule() { return "Thứ Ba hằng tuần, từ 20 giờ"; } }
const liveCourse = new LiveCourse("React thực chiến", 10);

// Lấy prototype ra từng tầng một
const step1 = Object.getPrototypeOf(liveCourse);
const step2 = Object.getPrototypeOf(step1);
const step3 = Object.getPrototypeOf(step2);
console.log(step1 === LiveCourse.prototype);   // true
console.log(step2 === Course.prototype);       // true
console.log(step3 === Object.prototype);       // true
console.log(Object.getPrototypeOf(step3));     // null (cuối chuỗi)

// summary được tìm thấy ở tầng thứ hai, Course.prototype
console.log(liveCourse.summary());             // React thực chiến (10 giờ)
Thứ tự tìm summary trên chuỗi
liveCourse(instance)Có title, hoursKhông có summaryLiveCourse.prototypeCó scheduleKhông có summaryCourse.prototypeCó summaryTìm thấy ở đâyObject.prototypeCó các phươngthức có sẵn(kế tiếp là null)
summary được tìm từ trên xuống và thấy ở Course.prototype. Chuỗi kết thúc ở null, ngay sau Object.prototype.

Chuỗi chứa Course.prototype, chứ không chứa bản thân Course. Phương thức có static nằm trên Course, nên khi đọc nó từ instance, JavaScript tìm đến tận null mà không thấy và cho ra undefined; còn nếu gọi thì sẽ báo lỗi TypeError.

Key cùng tên trên instance được tìm thấy trước

Nếu bạn gán liveCourse.summary = "Sắp ra mắt";, tên sẽ được tìm thấy ngay trên liveCourse, nên JavaScript không tìm tiếp lên prototype nữa. Khi đó liveCourse.summary() báo lỗi TypeError: liveCourse.summary is not a function.

Hiển thị thứ tự tìm phương thức cho từng loại xe. Car, ElectricCar, compactCar, evprototypeNames đã được khai báo sẵn.

① Hãy hiển thị xem prototype của ev có phải là ElectricCar.prototype không.

② Hãy định nghĩa chainOf, nối các tên từ prototype của xe được truyền vào cho đến ngay trước null.

③ Hãy hiển thị kết quả cho compactCarev.

(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

Chỉ xét key của chính instance — hasOwnProperty

Khi nhận dữ liệu khóa học, có thể bạn muốn kiểm tra xem summary có bị ghi đè trên từng khóa học bằng phép gán trực tiếp hay không. "summary" in jsCoursetrue kể cả với phương thức nằm trên prototype, nên không cho biết giá trị có được gán vào chính instance hay không.

hasOwnProperty (phương thức chỉ trả về true khi chính object có key đó) nhận key dưới dạng chuỗi, như jsCourse.hasOwnProperty("title"). Bản thân phương thức này nằm trên Object.prototype và được tìm thấy qua chuỗi.

class Course {
  constructor(title, hours) { this.title = title; this.hours = hours; }
  summary() { return `${this.title} (${this.hours} giờ)`; }
}
const jsCourse = new Course("JS cơ bản", 12);
const sqlCourse = new Course("SQL cơ bản", 8);
sqlCourse.summary = "Sắp ra mắt";   // Chỉ ghi đè cho khóa học này

// in vẫn là true với tên nằm trên prototype
console.log("summary" in jsCourse);                        // true
console.log("summary" in sqlCourse);                       // true

// hasOwnProperty chỉ true với key mà chính khóa học đó có
console.log(jsCourse.hasOwnProperty("summary"));           // false
console.log(sqlCourse.hasOwnProperty("summary"));          // true (đã bị ghi đè)
console.log(Course.prototype.hasOwnProperty("summary"));   // true
in và hasOwnProperty tìm ở đâu
Phạm vi "summary" in jsCourse tìm (cả chuỗi)
Chính jsCourse (hasOwnProperty chỉ xét ở đây)
  • title"JS cơ bản"
  • hours12
Course.prototype
  • summary()true với in, false với jsCourse.hasOwnProperty
Object.prototype
  • hasOwnProperty()jsCourse gọi được vì nó được tìm thấy ở đây
in xét cả chuỗi, còn hasOwnProperty chỉ xét trong khung jsCourse. summary không nằm trong khung jsCourse.

Với sqlCourse, khóa học đã bị ghi đè bằng phép gán, hasOwnProperty("summary") trả về true, nên chỉ một dòng là đủ để lọc ra những khóa học bị ghi đè. Bảng dưới đây cho thấy các cách kiểm tra khác xử lý titlesummary ra sao.

Cách kiểm tratitle (của jsCourse)summary (trên Course.prototype)
Có trong Object.keys(jsCourse)?Không
Có trong for...in?Không
Course.prototype.hasOwnProperty("key")falsetrue

Thêm phương thức từ bên ngoài — gán vào prototype

Giờ giả sử các instance khóa học do code dùng chung của một nhóm khác tạo ra rồi chuyển cho bạn, nên bạn không sửa được phần thân class, cũng không sửa được các dòng new. Nếu gán hàm hiển thị trả về "Khóa học: tên" cho từng khóa học một, thì khóa học nào bị bỏ sót sẽ báo lỗi TypeError khi được gọi.

Nếu bạn gán một hàm vào prototype, như Course.prototype.label = function () { ... }, mọi instance sẽ tìm thấy cùng một label qua chuỗi. Nếu sửa được phần thân class, hãy viết phương thức ở đó.

class Course {
  constructor(title, hours) { this.title = title; this.hours = hours; }
  summary() { return `${this.title} (${this.hours} giờ)`; }
}
const jsCourse = new Course("JS cơ bản", 12);   // Tạo trước hai khóa học
const sqlCourse = new Course("SQL cơ bản", 8);

// Chỉ gán cho jsCourse thì sqlCourse không có label
jsCourse.label = function () { return `Khóa học: ${this.title}`; };
console.log(typeof sqlCourse.label);                // undefined (gọi sẽ báo lỗi TypeError)

// Gán một lần vào Course.prototype (viết bằng function vì có đọc this)
Course.prototype.label = function () { return `Khóa học: ${this.title}`; };
console.log(sqlCourse.label());                     // Khóa học: SQL cơ bản
console.log(sqlCourse.hasOwnProperty("label"));     // false

// Gom các key mà for...in lấy ra
const keys = [];
for (const key in sqlCourse) keys.push(key);
console.log(keys.join(", "));                       // title, hours, label
Gán label ở đâu quyết định kết quả
Chỉ gán labelcho jsCourseChính sqlCoursekhông có labelChuỗi cũng khôngcó: undefinedsqlCourse.label()báo lỗi TypeErrorGán vàoCourse.prototype.labelChính sqlCoursekhông có labelTìm thấy ởCourse.prototypeKhóa học:SQL cơ bản
Ở cả hai trường hợp, chính sqlCourse không có label. Chỉ khi gán vào Course.prototype, nó mới được tìm thấy qua chuỗi.

Ngay cả sqlCourse, được tạo trước phép gán, cũng tìm thấy nó, vì JavaScript chỉ tìm trên chuỗi vào lúc tên đó được dùng. Khác với summary viết trong thân class, label thêm bằng phép gán còn xuất hiện trong for...in, nên code duyệt qua các key sẽ thấy nó lẫn với các key dữ liệu.

Hãy viết hàm thêm vào prototype bằng function

Nếu viết Course.prototype.label = () => this.title;, arrow function sẽ dùng this ở bên ngoài class. Trong trình soạn thảo của trang này, giá trị đó là undefined, nên sqlCourse.label() báo lỗi TypeError. Hãy dùng function.

Bạn thêm phần hiển thị thời lượng vào danh sách phim mà không sửa phần thân của Video. Videovideos đã được khai báo sẵn.

① Hãy thêm durationLabel trả về "N giờ N phút" sao cho phim nào cũng gọi được.

② Với từng phim, hãy hiển thị "Tên phim: N giờ N phút".

③ Hãy hiển thị xem phim đầu tiên có tự có durationLabel hay không.

④ Hãy duyệt phim đầu tiên bằng for...in và hiển thị những key mà chính phim đó không có.

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 1Với phương thức summary viết trong class, jsCourse.hasOwnProperty("summary") trả về gì?

Câu 2Với LiveCourse extends Course, prototype của LiveCourse.prototype là gì?

Câu 3Đã có sẵn vài khóa học. Nếu sau đó bạn gán một label viết bằng function vào Course.prototype thì sao?