Trong lập trình, chúng ta thường muốn lấy một cái gì đó và mở rộng nó.
Chẳng hạn, chúng ta có một đối tượng user
với các thuộc tính và phương thức của nó, và muốn tạo admin
và guest
các biến thể được sửa đổi một chút của nó. Chúng ta muốn sử dụng lại những gì chúng ta có từuser
, không sao chép / thực hiện lại các phương thức của nó, chỉ cần xây dựng một đối tượng mới.
Kế thừa nguyên mẫu(Prototypal inheritance) là một tính năng ngôn ngữ giúp làm điều đó.
Nội dung chính
1. [[Nguyên mẫu]] ([[Prototype]])
Trong JavaScript, các đối tượng có một thuộc tính ẩn đặc biệt [[Prototype]]
(như được đặt tên trong đặc tả), đó là null
hoặc tham chiếu một đối tượng khác. Đối tượng đó được gọi là một mẫu nguyên mẫu:
Nguyên mẫu là một chút ma thuật huyền bí. Khi chúng ta muốn đọc một thuộc tính từ object
đó và nó bị thiếu, JavaScript sẽ tự động lấy nó từ nguyên mẫu. Trong lập trình, điều đó được gọi là thừa kế nguyên mẫu. Nhiều tính năng ngôn ngữ thú vị và kỹ thuật lập trình được dựa trên nó.
Thuộc tính [[Prototype]]
là nội bộ(private) và ẩn, nhưng có nhiều cách để thiết lập nó.
Một trong số đó là sử dụng tên đặc biệt __proto__
, như thế này:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
__proto__
là một getter / setter – [[Prototype]]
Xin lưu ý rằng __proto__
nó không giống như [[Prototype]]
. Đó là một getter / setter cho nó.
Nó tồn tại vì lý do lịch sử. Trong ngôn ngữ hiện đại, nó được thay thế bằng các hàm Object.getPrototypeOf/Object.setPrototypeOf
cũng có được / thiết lập nguyên mẫu. Chúng ta sẽ nghiên cứu lý do cho điều đó và các hàm này sau.
Theo đặc tả, __proto__
chỉ phải được hỗ trợ bởi các trình duyệt, nhưng trên thực tế tất cả các môi trường bao gồm phía máy chủ đều hỗ trợ nó. Hiện tại, vì ký hiệu__proto__
rõ ràng hơn một chút bằng trực giác, chúng ta sẽ sử dụng nó trong các ví dụ.
Nếu chúng ta tìm kiếm một thuộc tính rabbit
và nó bị thiếu, JavaScript sẽ tự động lấy nó từ đó animal
.
Ví dụ:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
Ở đây, dòng (*)
thiết lập animal
là một nguyên mẫu của rabbit
.
Sau đó, khi alert
cố gắng đọc thuộc tính rabbit.eats
(**)
, nó không ở trong rabbit
, vì vậy JavaScript theo [[Prototype]]
tham chiếu và tìm thấy nó trong animal
(nhìn từ dưới lên):
Ở đây chúng ta có thể nói rằng ” animal
là nguyên mẫu của rabbit
” hoặc ” rabbit
kế thừa nguyên mẫu từ animal
“.
Vì vậy, nếu animal
có nhiều thuộc tính và phương thức hữu ích, thì chúng sẽ tự động có sẵn trong rabbit
. Những tính chất như vậy được gọi là người thừa kế.
Nếu chúng ta có một phương thức trong animal
, nó có thể được gọi trên rabbit
:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// walk is taken from the prototype
rabbit.walk(); // Animal walk
Phương thức này được tự động lấy từ nguyên mẫu, như thế này:
Chuỗi kế thừa nguyên mẫu có thể dài hơn:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// walk is taken from the prototype chain
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (from rabbit)
Chỉ có hai hạn chế:
- Các tham chiếu không thể đi theo vòng tròn. JavaScript sẽ đưa ra lỗi nếu chúng ta cố gắng gán
__proto__
trong một vòng tròn. - Giá trị của
__proto__
có thể là một đối tượng hoặcnull
. Các loại khác được bỏ qua.
Ngoài ra nó có thể rõ ràng, nhưng vẫn: chỉ có thể có một [[Prototype]]
. Một đối tượng có thể không được thừa kế từ hai người khác.
2. Không thay đổi giá trị của sử dụng nguyên mẫu
Các nguyên mẫu chỉ được sử dụng để đọc thuộc tính.
Thao tác ghi / xóa làm việc trực tiếp với đối tượng.
Trong ví dụ dưới đây, chúng ta gán phương thức walk
riêng của mình cho rabbit
:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
let animal = {
eats: true,
walk() {
/* this method won't be used by rabbit */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
Từ giờ trở đi, rabbit.walk()
lệnh gọi sẽ tìm phương thức ngay lập tức trong đối tượng và thực thi nó mà không cần sử dụng nguyên mẫu:
Các thuộc tính của Accessor là một ngoại lệ, vì phép gán được xử lý bởi hàm setter. Vì vậy, viết cho một thuộc tính như vậy thực sự giống như gọi một hàm.
Vì lý do đó admin.fullName
hoạt động chính xác trong code dưới đây:
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith (*)
// setter triggers!
admin.fullName = "Alice Cooper"; // (**)
Ở đây trong dòng (*)
thuộc tính admin.fullName
có một getter trong nguyên mẫu user
, vì vậy nó được gọi. Và trong dòng (**)
thuộc tính có một setter trong nguyên mẫu, vì vậy nó được gọi.
3. Giá trị của “this”
Một câu hỏi thú vị có thể xuất hiện trong ví dụ trên: giá trị this
bên trong là set fullName(value)
gì? Trường hợp các thuộc tính this.name
và this.surname
được viết: vào user
hoặc admin
?
Câu trả lời rất đơn giản: hoàn toàn this
không bị ảnh hưởng bởi các nguyên mẫu.
Bất kể nơi nào phương thức được tìm thấy: trong một đối tượng hoặc nguyên mẫu của nó. Trong một lệnh gọi phương thức, this
luôn luôn là đối tượng trước dấu chấm.
Vì vậy, cuộc gọi setter admin.fullName=
sử dụng admin
như this
.
Đó thực sự là một điều cực kỳ quan trọng, bởi vì chúng ta có thể có một đối tượng lớn với nhiều phương thức và có các đối tượng kế thừa từ nó. Và khi các đối tượng kế thừa chạy các phương thức được kế thừa, chúng sẽ chỉ sửa đổi trạng thái của chính chúng chứ không phải trạng thái của đối tượng mà nó kế thừa.
Chẳng hạn, ở đây animal
đại diện cho một cách lưu trữ phương thức, và rabbit
sử dụng nó.
Lệnh gọi rabbit.sleep()
đặt this.isSleeping
trên đối tượngrabbit
:
// animal has methods
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
// modifies rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)
Hình ảnh kết quả:
Nếu chúng ta có các đối tượng khác, như bird
, snake
v.v., kế thừa từ đó animal
, chúng cũng sẽ có quyền truy cập vào các phương thức animal
. Nhưng this
trong mỗi phương thức, cuộc gọi sẽ là đối tượng tương ứng, được đánh giá tại thời điểm lệnh gọi (trước dấu chấm), chứ không phải animal
. Vì vậy, khi chúng ta ghi dữ liệu vào this
, nó được lưu trữ vào các đối tượng này.
Kết quả là các phương thức được chia sẻ, nhưng trạng thái đối tượng thì không.
4. vòng lặp for..in
Các vòng lặpfor..in
trên thừa hưởng.
Ví dụ:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys only returns own keys
alert(Object.keys(rabbit)); // jumps
// for..in loops over both own and inherited keys
for(let prop in rabbit) alert(prop); // jumps, then eats
Nếu đó không phải là những gì chúng ta muốn và chúng ta muốn loại trừ các thuộc tính được thừa kế, thì có một phương thức obj.hasOwnProperty(key) : nó trả về true
nếu obj
có thuộc tính riêng (không được thừa kế) key
.
Vì vậy, chúng ta có thể lọc ra các thuộc tính được kế thừa (hoặc làm một cái gì đó khác với chúng):
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
Ở đây chúng ta có chuỗi thừa kế sau: rabbit
kế thừa từ animal
, animal
kế thừa từ Object.prototype
(vì animal
là một đối tượng theo nghĩa đen {...}
, do đó, theo mặc định), và sau đó null
ở trên nó:
Lưu ý, có một điều buồn cười. Phương thức rabbit.hasOwnProperty
đến từ đâu? Chúng ta đã không định nghĩa nó. Nhìn vào chuỗi chúng ta có thể thấy rằng phương thức được cung cấp bởi Object.prototype.hasOwnProperty
. Nói cách khác, nó được kế thừa.
Nhưng tại sao hasOwnProperty
không xuất hiện trong for..in
vòng lặp giống eats
và jumps
, for..in
liệt kê các thuộc tính được kế thừa không?
Câu trả lời rất đơn giản: không liệt kê. Cũng giống như tất cả các thuộc tính khác của Object.prototype
, nó có cờ enumerable:false
. Và for..in
chỉ liệt kê các thuộc tính chỉnh sửa được. Đó là lý do tại sao nó và phần còn lại của thuộc tính Object.prototype
không được liệt kê.
Hầu như tất cả các phương thức nhận(get) khóa / giá trị khác đều bỏ qua các thuộc tính được kế thừa
Hầu như tất cả các phương thức nhận(get) khóa / giá trị khác, chẳng hạn như Object.keys
, Object.values
v.v đều bỏ qua các thuộc tính được kế thừa.
Họ chỉ hoạt động trên chính đối tượng. Các thuộc tính từ nguyên mẫu không được tính đến.
5. Tóm lược
- Trong JavaScript, tất cả các đối tượng có một thuộc tính ẩn
[[Prototype]]
đó là một đối tượng khác hoặcnull
. - Chúng ta có thể sử dụng
obj.__proto__
để truy cập nó (một getter / setter lịch sử, có nhiều cách khác, sẽ sớm được đề cập). - Đối tượng được tham chiếu bởi
[[Prototype]]
được gọi là một nguyên mẫu. - Nếu chúng ta muốn đọc một thuộc tính
obj
hoặc gọi một phương thức và nó không tồn tại, thì JavaScript sẽ cố gắng tìm nó trong nguyên mẫu. - Các thao tác ghi / xóa tác động trực tiếp lên đối tượng, chúng không sử dụng nguyên mẫu (giả sử đó là thuộc tính dữ liệu, không phải là setter).
- Nếu chúng ta gọi
obj.method()
, vàmethod
được lấy từ nguyên mẫu,this
vẫn tham chiếuobj
. Vì vậy, các phương thức luôn làm việc với đối tượng hiện tại ngay cả khi chúng được kế thừa. - Các vòng lặp
for..in
trên cả thuộc tính riêng của mình và thuộc tính thừa kế của nó. Tất cả các phương thức nhận(get) khóa / giá trị khác chỉ hoạt động trên chính đối tượng.
Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!