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 adminguestcá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 đó.

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à nullhoặ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__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.setPrototypeOfcũ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 rabbitvà 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 animallà một nguyên mẫu của rabbit.

Sau đó, khi alertcố 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 ” animallà nguyên mẫu của rabbit” hoặc ” rabbitkế thừa nguyên mẫu từ animal“.

Vì vậy, nếu animalcó 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ế:

  1. 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.
  2. Giá trị của __proto__có thể là một đối tượng hoặc null. 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.fullNamehoạ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.fullNamecó 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ị thisbên trong là set fullName(value)gì? Trường hợp các thuộc tính this.namethis.surnameđược viết: vào userhoặc admin?

Câu trả lời rất đơn giản: hoàn toàn thiskhô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, thisluô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 adminnhư 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à rabbitsử dụng nó.

Lệnh gọi rabbit.sleep()đặt this.isSleepingtrê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, snakev.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 thistrong 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ề truenếu objcó 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: rabbitkế thừa từ animal, animal kế thừa từ Object.prototype(vì animallà 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 hasOwnPropertykhông xuất hiện trong for..invòng lặp giống eatsjumps, for..inliệ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..inchỉ 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.valuesv.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ặc null.
  • 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 objhoặ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, thisvẫn tham chiếu obj. 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!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!