Câu 1Với phương thức summary viết trong class, jsCourse.hasOwnProperty("summary") trả về gì?
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ờ)
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 title và hours.
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ờ)
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.
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 jsCourse là true 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
title—"JS cơ bản"hours—12
summary()—truevớiin,falsevớijsCourse.hasOwnProperty
hasOwnProperty()—jsCoursegọi được vì nó được tìm thấy ở đây
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ý title và summary ra sao.
| Cách kiểm tra | title (của jsCourse) | summary (trên Course.prototype) |
|---|---|---|
| Có trong Object.keys(jsCourse)? | Có | Không |
| Có trong for...in? | Có | Không |
| Course.prototype.hasOwnProperty("key") | false | true |
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
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.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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?