Tính kế thừa của lớp là một cách để một lớp mở rộng từ một lớp khác.
Vì vậy, chúng ta có thể tạo phương thức mới trên các đối tượng hiện có.
Nội dung chính
1. Từ khóa “extends”
Chúng ta có lớp Animal
:
/*
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/
*/
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
let animal = new Animal("My animal");
Đây là cách chúng ta có thể biểu diễn đối tượng animal
và Sơ đồ của lớp Animal
:
Và tôi muốn tạo ra một cái khác class Rabbit
.
Vì thỏ là động vật, lớpRabbit
nên dựa vào Animal
, có quyền truy cập vào các phương thức của động vật, để thỏ có thể làm những gì mà động vật có tên chung chung có thể làm.
Cú pháp để mở rộng một lớp khác là : class Child extends Parent
.
Hãy tạo ra class Rabbit
những kế thừa từ Animal
:
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!
Đối tượng của lớpRabbit
có quyền truy cập vào cả hai phương thức của Rabbit
, chẳng hạn như rabbit.hide()
và cả các phương thứcAnimal
, chẳng hạn như rabbit.run()
.
Trong nội bộ, từ khóa extends
hoạt động bằng cách sử dụng các cơ chế của nguyên mẫu(prototype). Nó đặt Rabbit.prototype.[[Prototype]]
thành Animal.prototype
. Vì vậy, nếu không tìm thấy một phương thức Rabbit.prototype
, JavaScript sẽ lấy nó từ đó Animal.prototype
.
Chẳng hạn, để tìm phương thức rabbit.run
, động cơ sẽ kiểm tra (từ dưới lên trên hình):
- Đối tượng
rabbit
(không córun
). - Nguyên mẫu của nó, đó là
Rabbit.prototype
(cóhide
, nhưng khôngrun
). - Nguyên mẫu của nó, nghĩa là (do
extends
)Animal.prototype
, cuối cùng cũng có phương thứcrun
.
Như chúng ta có thể nhớ lại từ chương Native prototypes, JavaScript tự sử dụng tính kế thừa nguyên mẫu cho các đối tượng. Ví dụ như Date.prototype.[[Prototype]]
là Object.prototype
. Đó là lý do tại sao ngày có quyền truy cập vào các phương thức đối tượng chung.
Bất kỳ biểu thức nào cũng được cho phép extends
Cú pháp class cho phép chỉ định không chỉ một lớp, mà bất kỳ biểu thức nào cũng đi sau từ khoáextends
.
Ví dụ, một lệnh gọi hàm tạo lớp cha:
function f(phrase) {
return class {
sayHi() { alert(phrase) }
}
}
class User extends f("Hello") {}
new User().sayHi(); // Hello
Ở đây class User
kế thừa từ kết quả của f("Hello")
.
Điều đó có thể hữu ích cho các mẫu lập trình nâng cao khi chúng ta sử dụng các hàm để tạo các lớp tùy thuộc vào nhiều điều kiện và có thể kế thừa từ chúng.
2. Ghi đè(Overriding) một phương thức
Bây giờ chúng ta hãy tiến lên và ghi đè(Overriding) một phương thức. Theo mặc định, tất cả các phương thức không được chỉ định trong class Rabbit
đều được thực hiện trực tiếp như là từ class Animal
.
Nhưng nếu chúng ta chỉ định phương thức riêng của mình trong Rabbit
, chẳng hạn như stop()
nó sẽ được sử dụng thay thế:
class Rabbit extends Animal {
stop() {
// ...now this will be used for rabbit.stop()
// instead of stop() from class Animal
}
}
Thông thường chúng ta không muốn thay thế hoàn toàn một phương thức cha, mà là xây dựng lại phương thức cha để điều chỉnh hoặc mở rộng hàm của nó. Chúng ta làm một cái gì đó trong phương thức của chúng ta, nhưng gọi phương thức cha trước / sau nó hoặc trong quy trình.
Các lớp cung cấp từ khóa "super"
để làm điều đó.
super.method(...)
để gọi một phương thức cha.super(...)
để gọi một hàm tạo của cha (chỉ bên trong hàm tạo của chúng ta).
Ví dụ, hãy để thỏ của chúng ta tự động khi dừng lại:
/*
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/
*/
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
stop() {
super.stop(); // call parent stop
this.hide(); // and then hide
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stands still. White rabbit hides!
Bây giờ Rabbit
có phương thức stop
và nó gọi phương thức cha super.stop()
trong quá trình.
Hàm mũi tên không có super
Các hàm mũi tên không có super
.
Nếu được truy cập, nó được lấy từ hàm bên ngoài. Ví dụ:
class Rabbit extends Animal {
stop() {
setTimeout(() => super.stop(), 1000); // call parent stop after 1sec
}
}
Hàm super
trong mũi tên giống như trong stop()
, vì vậy nó hoạt động như dự định. Nếu chúng ta đã chỉ định một hàm thông thường của người dùng ở đây, thì sẽ xảy ra lỗi:
// Unexpected super
setTimeout(function() { super.stop() }, 1000);
3. Overriding constructor
Với các constructor, nó có một chút khó khăn.
Cho đến bây giờ, Rabbit
không có constructor
riêng của nó .
Theo đặc tả , nếu một lớp mở rộng một lớp khác và không có constructor
, thì lớp trống rỗng sau đây được tạo ra một constructor
:
class Rabbit extends Animal {
// generated for extending classes without own constructors
constructor(...args) {
super(...args);
}
}
Như chúng ta có thể thấy, về cơ bản nó gọi constructor
cha truyền cho nó tất cả các đối số. Điều đó xảy ra nếu chúng ta không viết một constructor
của riêng lớp con.
Bây giờ hãy thêm một hàm tạo tùy chỉnh của mình vào Rabbit
. Nó sẽ tạo thêm earLength
ngoài thuộc tínhname
:
/*
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/
*/
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
this.speed = 0;
this.name = name;
this.earLength = earLength;
}
// ...
}
// Doesn't work!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
Rất tiếc! Chúng ta đã có một lỗi. Bây giờ chúng ta không thể tạo ra thỏ. Có chuyện gì?
Câu trả lời ngắn gọn là: các hàm xây dựng(constructors) trong kế thừa các lớp phải gọi super(...)
và phải Làm điều đó trước khi sử dụng this
.
…Nhưng tại sao? Những gì đang xảy ra ở đây? Thật vậy, yêu cầu có vẻ lạ.
Tất nhiên, có một lời giải thích. Hãy đi vào chi tiết, vì vậy bạn sẽ thực sự hiểu những gì đang diễn ra.
Trong JavaScript, có một sự khác biệt giữa hàm xây dựng(constructors) của một lớp kế thừa (cái gọi là hàm tạo của lớp dẫn xuất hay hàm tạo của lớp con) và các hàm khác. Một constructor dẫn xuất có một thuộc tính nội bộ đặc biệt [[ConstructorKind]]:"derived"
. Đó là một nhãn nội bộ đặc biệt.
Nhãn đó ảnh hưởng đến hành vi của nó với new
.
- Khi một hàm thông thường được thực thi
new
, nó sẽ tạo một đối tượng trống và gán nó chothis
. - Nhưng khi một hàm tạo dẫn xuất chạy, nó không làm điều này. Nó hy vọng các hàm xây dựng(constructors) cha sẽ làm công việc này.
Vì vậy, một hàm tạo có nguồn gốc phải gọi super
để thực thi hàm tạo (không dẫn xuất) của nó, nếu không thì đối tượng this
sẽ không được tạo. Và chúng ta sẽ nhận được một lỗi.
Để hàm tạo Rabbit
hoạt động, nó cần gọi super()
trước khi sử dụng this
, như ở đây:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.earLength = earLength;
}
// ...
}
// now fine
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10
4. Super: nội bộ(internals), [[HomeObject]]
Thông tin nâng cao
Nếu bạn đang học bài hướng dẫn này lần đầu tiên – phần này có thể bị bỏ qua.
Đó là về các cơ chế nội bộ đằng sau sự kế thừa và super
.
Chúng ta hãy đi sâu hơn một chút` của super
. Chúng ta sẽ thấy một số điều thú vị trên đường đi.
Đầu tiên phải nói, từ tất cả những gì chúng ta đã học cho đến bây giờ, nó không thể làm chosuper
làm việc được!
Vâng, thực sự, chúng ta hãy tự hỏi, làm thế nào nó nên hoạt động về mặt kỹ thuật? Khi một phương thức đối tượng chạy, nó nhận được đối tượng hiện tại là this
. Nếu chúng ta gọi super.method()
thì Công cụ của Javascript cần lấy method
từ nguyên mẫu của đối tượng hiện tại. Nhưng bằng cách nào?
Nhiệm vụ có vẻ đơn giản, nhưng không phải. Công cụ biết đối tượng this
hiện tại , vì vậy nó có thể lấy method
cha là this.__proto__.method
. Thật không may, một giải pháp này không hoạt động.
Hãy chứng minh vấn đề. Không có các lớp, sử dụng các đối tượng đơn giản vì mục đích đơn giản.
Bạn có thể bỏ qua phần này và đi đến phần phụ[[HomeObject]]
nếu bạn không muốn biết chi tiết. Điều đó sẽ không gây hại. Hoặc đọc tiếp nếu bạn muốn tìm hiểu sâu về những điều sâu sắc.
Trong ví dụ dưới đây , rabbit.__proto__ = animal
. Bây giờ hãy thử: rabbit.eat()
chúng ta sẽ gọi animal.eat()
, sử dụng this.__proto__
:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() {
// that's how super.eat() could presumably work
this.__proto__.eat.call(this); // (*)
}
};
rabbit.eat(); // Rabbit eats.
Tại dòng (*)
chúng ta lấy eat
từ nguyên mẫu ( animal
) và gọi nó trong ngữ cảnh của đối tượng hiện tại. Xin lưu ý rằng điều này rất quan trọng ở đây .call(this)
, bởi vì đơn giản this.__proto__.eat()
sẽ thực thi eat
cha trong ngữ cảnh của nguyên mẫu, không phải đối tượng hiện tại.
Và trong đoạn code trên nó thực sự hoạt động như dự định: chúng ta có chính xác alert
.
Bây giờ hãy thêm một đối tượng vào chuỗi. Chúng ta sẽ thấy mọi thứ vỡ như thế nào:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
eat() {
// ...bounce around rabbit-style and call parent (animal) method
this.__proto__.eat.call(this); // (*)
}
};
let longEar = {
__proto__: rabbit,
eat() {
// ...do something with long ears and call parent (rabbit) method
this.__proto__.eat.call(this); // (**)
}
};
longEar.eat(); // Error: Maximum call stack size exceeded
Code không hoạt động nữa! Chúng ta có thể thấy lỗi khi cố gắng gọi longEar.eat()
.
Nó có thể không rõ ràng, nhưng nếu chúng ta theo dõi lệnh gọilongEar.eat()
, thì chúng ta có thể thấy lý do tại sao. Trong cả hai dòng (*)
và (**)
giá trị của this
là đối tượng hiện tại ( longEar
). Điều đó rất cần thiết: tất cả các phương thức đối tượng đều lấy đối tượng hiện tại this
, không phải là nguyên mẫu hay thứ gì đó.
Vì vậy, trong cả hai dòng (*)
và (**)
giá trị của this.__proto__
là hoàn toàn giống nhau : rabbit
. Cả hai gọi rabbit.eat
mà không đi lên chuỗi xử lý trong vòng lặp vô tận.
Đây là bức tranh về những gì xảy ra:
- Bên trong
longEar.eat()
, các(**)
gọirabbit.eat
cung cấp cho nó vớithis=longEar
.
// inside longEar.eat() we have this = longEar
this.__proto__.eat.call(this) // (**)
// becomes
longEar.__proto__.eat.call(this)
// that is
rabbit.eat.call(this);
2. Sau đó, trong dòng (*)
của rabbit.eat
, chúng ta muốn vượt qua lệnh gọi thậm chí cao hơn trong chuỗi xử lý, nhưng this=longEar
, vì vậy this.__proto__.eat
là một lần nữa rabbit.eat
!
// inside rabbit.eat() we also have this = longEar
this.__proto__.eat.call(this) // (*)
// becomes
longEar.__proto__.eat.call(this)
// or (again)
rabbit.eat.call(this);
3. Vì vậy, bản thân rabbit.eat
gọi nó trong vòng lặp vô tận, bởi vì nó không thể tăng thêm nữa.
Vấn đề không thể được giải quyết bằng cách sử dụng this
một mình.
[[HomeObject]]
Để cung cấp giải pháp, JavaScript thêm một thuộc tính nội bộ đặc biệt cho các hàm : [[HomeObject]]
.
Khi một hàm được chỉ định là một phương thức lớp hoặc đối tượng, thuộc tính của nó [[HomeObject]]
trở thành đối tượng đó.
Sau đó super
sử dụng nó để giải quyết nguyên mẫu cha và các phương thức của nó.
Chúng ta hãy xem cách nó hoạt động, đầu tiên với các đối tượng đơn giả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 = {
name: "Animal",
eat() { // animal.eat.[[HomeObject]] == animal
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() { // rabbit.eat.[[HomeObject]] == rabbit
super.eat();
}
};
let longEar = {
__proto__: rabbit,
name: "Long Ear",
eat() { // longEar.eat.[[HomeObject]] == longEar
super.eat();
}
};
// works correctly
longEar.eat(); // Long Ear eats.
Nó hoạt động như dự định, do [[HomeObject]]
cơ học. Một phương thức, chẳng hạn như longEar.eat
, biết phương thức của nó [[HomeObject]]
và lấy phương thức cha từ nguyên mẫu của nó. Mà không sử dụng this
.
4.1. Các phương thức không phải là “miễn phí” để muốn làm gì thì làm
Như chúng ta đã biết trước đây, các phương thức nói chung là miễn phí, không bị ràng buộc với các đối tượng trong JavaScript. Vì vậy, chúng có thể được sao chép giữa các đối tượng và được gọi với một đối tượng khác this
.
Chính sự tồn tại của [[HomeObject]]
vi phạm nguyên tắc đó, bởi vì các phương thức ghi nhớ các đối tượng của chúng. [[HomeObject]]
không thể thay đổi.
Nơi duy nhất trong ngôn ngữ [[HomeObject]]
được sử dụng – là super
. Vì vậy, nếu một phương thức không sử dụng super
, thì chúng ta vẫn có thể xem xét nó miễn phí và sao chép giữa các đối tượng. Nhưng với super
những điều có thể đi sai.
Đây là bản demo của một super
kết quả sai sau khi sao chép:
let animal = {
sayHi() {
console.log(`I'm an animal`);
}
};
// rabbit inherits from animal
let rabbit = {
__proto__: animal,
sayHi() {
super.sayHi();
}
};
let plant = {
sayHi() {
console.log("I'm a plant");
}
};
// tree inherits from plant
let tree = {
__proto__: plant,
sayHi: rabbit.sayHi // (*)
};
tree.sayHi(); // I'm an animal (?!?)
Một lệnh gọi cho tree.sayHi()
thấy tôi là một động vật. Chắc chắn là sai.
Lý do rất đơn giản:
- Trong dòng
(*)
, phương thứctree.sayHi
được sao chép từrabbit
. Có lẽ chúng ta chỉ muốn tránh trùng lặp code? - Nó
[[HomeObject]]
làrabbit
, như nó đã được tạo ra trongrabbit
. Không có cách nào để thay đổi[[HomeObject]]
. - Code của
tree.sayHi()
cósuper.sayHi()
bên trong. Nó đi lên từrabbit
và lấy phương thức từanimal
.
Đây là sơ đồ của những gì xảy ra:
4.2. Phương thức, không phải là thuộc tính hàm
[[HomeObject]]
được định nghĩa cho các phương thức cả trong các lớp và trong các đối tượng đơn giản. Nhưng đối với các đối tượng, các phương thức phải được chỉ định chính xác method()
, không phải là "method: function()"
.
Sự khác biệt có thể không cần thiết đối với chúng ta, nhưng nó quan trọng đối với JavaScript.
Trong ví dụ dưới đây, một cú pháp phi phương thức được sử dụng để so sánh. [[HomeObject]]
thuộc tính không được đặt và thừa kế không hoạt động:
let animal = {
eat: function() { // intentionally writing like this instead of eat() {...
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
super.eat();
}
};
rabbit.eat(); // Error calling super (because there's no [[HomeObject]])
5. Tóm lược
- Để mở rộng một lớp
class Child extends Parent
::- Điều đó có nghĩa
Child.prototype.__proto__
làParent.prototype
, vì vậy các phương thức được kế thừa.
- Điều đó có nghĩa
- Khi ghi đè một hàm tạo:
- Chúng ta phải gọi hàm tạo cha như
super()
trong hàmChild
tạo trước khi sử dụngthis
.
- Chúng ta phải gọi hàm tạo cha như
- Khi ghi đè phương thức khác:
- Chúng ta có thể sử dụng
super.method()
trong mộtChild
phương thức để gọiParent
phương thức.
- Chúng ta có thể sử dụng
- Nội bộ:
- Các phương thức ghi nhớ lớp / đối tượng của chúng trong thuộc tính bên trong
[[HomeObject]]
. Đó là cáchsuper
giải quyết các phương thức cha. - Vì vậy, không an toàn khi sao chép một phương thức
super
từ đối tượng này sang đối tượng khác.
- Các phương thức ghi nhớ lớp / đối tượng của chúng trong thuộc tính bên trong
Cũng như vậy:
- Các hàm mũi tên không có riêng
this
hoặcsuper
, vì vậy chúng hoàn toàn phù hợp với bối cảnh hiện tại.
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!