Khái niệm về kế thừa trong Javascript có thể gây nhầm lẫn với developer đã từng làm việc với ngôn ngữ hướng đối tượng như Java, C++. Từ khoá class được javascript giới thiệu trong bản ES5. Về cơ bản thì Class trong javascript chỉ mang tính tượng hình, bản chất của nó chỉ là prototype-based mà thôi. Mình sẽ chứng minh ở phía dưới! Cùng đọc nhé.
Các khái niệm:
Class-based programming:
Việc kế thừa thông qua các class. Các class được define ra đại diện cho một Object.
Ví dụ: class Customer sẽ kế thừa từ class Person, nên sẽ có các thuộc tính và phương thức của class Person.
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Customer extends Person {
constructor(name) {
super(name);
}
}
Prototype-based
Prototype là nguyên mẫu, nói theo ngôn ngữ Javascript thì một Object tạo luôn có một nguyên mẫu. Nói theo ngôn ngữ OOP thì việc tạo ra từ một nguyên mẫu giống như kế thừa từ một class nào đó.
Trong javascript việc kế thừa thông các prototype (nguyên mẫu). Theo mặc định, mỗi object trong javascript tạo ra đều có prototype của Object.
Object tạo ra thể truy xuất đến prototyp thông qua property __proto__ hoặc function Object.getPrototypeOf(#yourObject).
Thuộc tính của Object prototype:
Nói theo ngôn ngữ OOP, xem object là class, thì mỗi class được tạo ra đều được kế thừa từ class Object . Kiểu Object ở đây là ông tổ á.
Ví dụ: Object user sẽ có prototype là Object.prototype.
let user = { name: "Ly Nhan"}
Object.getPrototypeOf(user)===Object.prototype
// true
Object admin sẽ có prototype là user
let user = { name: "Ly Nhan"}
Object.getPrototypeOf(user)===Object.prototype
// true
let admin =Object.create(user)
Object.getPrototypeOf(admin) === user
//true
Prototype-chain:
Mỗi Object trong javascript sẽ có một private property (__proto__) trỏ tới một object (prototype). Vì prototype này cũng là một object, nên nó cũng có một private property (__proto__) để trỏ tới một object (prototype). Và việc này cứ diễn ra cho tới khi nó trỏ tới một prototype null.
Vì mọi Object tạo ra đều có Object.prototype làm ông tổ. Nên đây sẽ là thằng nằm cuối cùng trong chuỗi. Có nghĩa là Object.prototype.__proto__ === null.
Ví dụ:
let user = {name:'Ly Nhan'}
// user.__proto__ ->> Object.prototype
user.__proto__ === Object.prototype //true
let student = Object.create(user)
//student.__proto__ ->> user
student.__proto__ === user // true
Inheritance with the prototype chain
Inheritance properties
Object trong javascript là dynamic, bạn có thể thêm mới, override, xoá các property. Khi object truy xuất tới một property, nó sẽ đi qua tất cả các property của prototype để tìm kiếm cho đến khi tìm được.
Ở hình trên mình tạo ra object student từ object user, có nghĩa object user là prototype của object student.
Lúc console.log() ra object student không có thuộc tính nào hết. Tuy nhiên user là prototype của student. Giả sử mình muốn truy xuất vào thuộc tính student.a | student.b nó sẽ tìm trong properties hiện của object student, nếu không có nó sẽ tìm kiếm trong object prototype user:
student prototype ở đây chính là object user:
Prototype của function
Lúc đầu rất dễ nhầm dẫn giữa prototype của Object và prototype của function.
Function.prototype: Là một object. Function thực thi với từ khoá new sẽ return về một function instance là một object, và object này có prototype là Function.prototype Giống như class, khi thực thi qua từ khoá new Class() thì sẽ trả về một class instance, tương tự với function, khi thực thi function với từ khoá new cũng trả về một object instance và instance này có prototype là Function.prototype
Object prototype: là một prototype(nguyên mẫu/class cha) của object khá (__proto__,/class con).
Chắc các bạn đã gặp đâu đó đoạn code kiểu như này:
let User = function (name) {
this.name = name;
};
User.prototype.sayHi = function () {
console.log(`I'm ${this.name}`);
};
Cùng xem User.prototype có gì nào:
Mình sẽ khởi tạo một function instance từ function User:
Struct của instance user như sau:
name: là property của object
__proto__: trỏ tới User.prototype được define ở trên.
let User = function (name) {
this.name = name;
};
//thêm function sayHi vào function prototype để khi thực thi
// những function instance có thể sử dụng được
User.prototype.sayHi = function () {
console.log(`I'm ${this.name}`);
};
let user = new User("Ly Nhan");
let user2 = new User("NextLint");
user.sayHi(); // I"m Ly Nhan
user2.sayHi(); // I"m NextLint
Magic chưa xuất hiện ở đây đâu, giả sửa mình có object có property name mà vẫn muốn sử dụng function này thì sao. ???
Cùng đi qua section tiếp theo mình sẽ nói đến việc kế thừa các phương thức.
Inheriting "methods"
Lúc nãy mình có đề cập tới việc một function khi thực thi với từ khoá new sẽ trả về một object, và nhận Function.prototype làm prototype. Như vậy object đã có rồi, việc còn lại là mình sẽ mượn Function.prototype để làm prototype cho object thôi.
User.prototype.sayHi = function () {
console.log(`I'm ${this.name}`);
};
let user = new User("Ly Nhan");
let user2 = new User("NextLint");
let obj = {
name: "I want to say HIIIII",
};
user.sayHi();
user2.sayHi();
Object.setPrototypeOf(obj, User.prototype);
obj.sayHi(); // I'm I want to say HIIII
Mình sẽ mượn prototype của function bằng cách set prototype của obj bằng User.prototype
Nếu như vậy thì obj.sayHi() === obj.__proto__.sayHi(). Mình cũng không biết nữa, cùng thử xem sao:
Oh...hmmmm... có gì khó hiểu ở đây nhỉ? object obj không có function sayHi, sau khi set prototype của nó bằng User.prototype thì lúc này nó có thể sử dụng được function sayHi. Nhưng khi bạn gọi function từ obj.__proto__.sayHi() lại là undefined.
...........lạ nhỉ ?
Khi bạn gọi obj.sayHi() thì sayHi đóng vai trò là method của obj này, nên nó có thể truy xuất được các properties của obj.
Giả thuyết của mình đưa ra như sau:
Khi bạn goi obj.__proto__.sayHi() là bạn đang gọi tới object prototype. Ở đây chỉ có function sayHi thôi không có property name nên sẽ print ra I'm undefined.
Để kiểm chứng việc này thì mình sẽ thêm một property name vào User.prototype và gọi lại obj.__proto__.sayHi() xem kết quả thử nha.
Yup.... Giả thuyết mình đưa ra là đúng. Lúc này User.prototype đã có property name nên khi mình gọi method sayHi() của prototype này sẽ sử dụng property này. Việc này không ảnh hưởng đến kết quả mình gọi obj.sayHi(). Đố các bạn biết tại sao ? Cùng suy nghĩ là trả lời ở phần bình luận nha ? ? ?
Cool. Cơ mà mình vẫn có một thắc mắc, là trường hợp obj sử dụng method của User.prototype truy xuất đến 1 property không có trong object thì sao. Giả sử mình thêm method whatAge và property age vào User.prototype:
let User = function (name) {
this.name = name;
};
User.prototype.sayHi = function () {
console.log(`I'm ${this.name}`);
};
User.prototype.whatAge = function () {
console.log(`I'm ${this.age}`);
};
User.prototype.age = 25
Ok giờ xem thử obj.whatAge() sẽ ra gì nha:
Bummm. Mặc gì không có property age những vẫn in ra được I'm 25.
Như vậy thì mình kết luận ra được là:
Khi set prototype của obj là User.prototype thì obj có thể sử dụng được các property và method của User.prototype. Và việc sử dụng này cũng có priority. Nếu bạn gọi một hàm từ obj['property]thì nó sẽ tìm trong properties của obj trước, nếu không có mới tìm kiếm trong prototype của obj.
Tại sao mình cần biết những kiến thức này:
Đa số framework hay javascript compiler cho phép các bạn viết code ES5,ES6, ES7, Typescript bla bla. Nhưng sau khi compile ra thì chỉ là những đoạn code ở trên thôi.
Đây là 1 ví dụ mình viết một class bằng typescript sau khi compile ra:
Đoạn code compile ở phía bên trái thật ra chỉ là prototype, ngay cả khi bạn viết một private function bằng typescript khi compile ra nó cũng chỉ là một method bình thường của Ninja.prototype thôi. Type chỉ có ý nghĩa ở compile time.
Vậy thì kế thừa nó sẽ như nào nhỉ?
Ở đoạn code trên sau khi build ra chỉ còn lại function, không có class nào ở đây cả.
function Admin nhận User ở dạng tham số. Và quá trình transfer properties + method diễn ra tại function __extend(Admin,_super).
var Admin = /** @class */ (function (_super) {
// _super ở đây chính là User
__extends(Admin, _super); // bắt đầu transfer prototype
function Admin(name, age) {
var _this = _super.call(this, name) || this;
_this._age = age;
return _this;
}
Admin.prototype.printAge = function () {
console.log("this age :", this._age);
};
return Admin;
})(User);
Và đây là nơi diễn ra việc kế thừa:
Nhìn hơi choảng phải không. Nhưng mình tin khi bạn đọc được rồi, bạn sẽ không ngán bất cứ code nào của javascript nữa. ?
Object trong javascript là dynamic, không có class, tất cả khởi tạo lúc runtime, thậm chí bạn có thể thay đổi thuộc tính của function tại runtime. Một đặc điểm quan trọng để viết meta programing trong javascript.
Bài viết đến đây là kết thúc, cảm ơn các bạn đã theo dõi. Nếu các bạn có thêm thông tin hay muốn bàn luận thêm thì hãy comment nha. ? ? ? ?