Prototype Inheritance (hay kế thừa prototype) trong JavaScript là cơ chế mà các đối tượng có thể “thừa hưởng” các thuộc tính và phương thức từ các đối tượng khác thông qua chuỗi prototype. Đây là một tính năng quan trọng trong mô hình lập trình hướng đối tượng (OOP) của JavaScript.

Cách hoạt động của Prototype Inheritance

Trong JavaScript, mỗi đối tượng có một thuộc tính ẩn gọi là [[Prototype]], nó tham chiếu đến một đối tượng khác, thường được gọi là prototype. Khi bạn truy cập một thuộc tính hoặc phương thức của đối tượng, JavaScript sẽ kiểm tra xem đối tượng có thuộc tính hoặc phương thức đó không. Nếu không, nó sẽ tìm kiếm trong đối tượng prototype mà đối tượng này liên kết đến. Quá trình tìm kiếm này tiếp diễn cho đến khi tìm thấy thuộc tính hoặc phương thức, hoặc cho đến khi prototype cuối cùng (thường là null) được kiểm tra.

Ví dụ về Prototype Inheritance

1. Tạo constructor function và prototype

function Person(name, age) {
    this.name = name;
    this.age = age;
}

// Thêm một phương thức vào prototype của Person
Person.prototype.sayHello = function() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

Trong ví dụ trên:

  • Person là một hàm khởi tạo (constructor function).
  • Phương thức sayHello được thêm vào prototype của Person, nghĩa là tất cả các đối tượng được tạo từ Person sẽ có quyền truy cập vào phương thức này thông qua kế thừa prototype.

2. Tạo một đối tượng và sử dụng kế thừa prototype

const person1 = new Person("Alice", 25);
person1.sayHello(); // "Hello, my name is Alice and I am 25 years old."

Ở đây:

  • Khi person1.sayHello() được gọi, JavaScript sẽ tìm kiếm phương thức sayHello trong đối tượng person1. Vì person1 không có phương thức này trực tiếp, JavaScript sẽ kiểm tra trong prototype của person1, đó chính là prototype của Person. Tại đây, nó sẽ tìm thấy sayHello và thực thi nó.

3. Liên kết chuỗi prototype

console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
  • person1.__proto__ (hoặc [[Prototype]]) trỏ tới Person.prototype.
  • Person.prototype.__proto__ trỏ tới Object.prototype, vì mọi đối tượng trong JavaScript cuối cùng đều kế thừa từ Object.
  • Chuỗi kế thừa tiếp tục cho đến khi prototype của Objectnull.

Một ví dụ khác về Prototype Inheritance

function Animal(type) {
    this.type = type;
}

Animal.prototype.getType = function() {
    return this.type;
};

function Dog(name) {
    this.name = name;
}

// Kế thừa từ Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
    console.log("Woof! My name is " + this.name);
};

const myDog = new Dog("Buddy");
myDog.type = "Dog";
myDog.bark(); // "Woof! My name is Buddy"
console.log(myDog.getType()); // "Dog"

Giải thích:

  • Hàm khởi tạo Animal có một thuộc tính type và một phương thức getType trong prototype.
  • Hàm khởi tạo Dog có một thuộc tính name. Nó kế thừa từ Animal bằng cách sử dụng Object.create(Animal.prototype), nghĩa là Dog có quyền truy cập vào tất cả các phương thức của Animal.
  • Phương thức bark được thêm trực tiếp vào prototype của Dog.

Lưu ý về constructor:

  • Sau khi sử dụng Object.create để kế thừa từ Animal, giá trị của Dog.prototype.constructor sẽ bị thay đổi. Do đó, ta cần gán lại giá trị Dog.prototype.constructor = Dog để đảm bảo đối tượng được tạo từ Dog sẽ có constructor đúng.

Cơ chế Prototype Chain

Khi bạn truy cập một thuộc tính hoặc phương thức của một đối tượng, JavaScript sẽ duyệt qua chuỗi prototype (prototype chain). Quá trình này diễn ra như sau:

  1. JavaScript kiểm tra đối tượng trực tiếp để tìm thuộc tính hoặc phương thức.
  2. Nếu không tìm thấy, nó kiểm tra prototype của đối tượng đó (thông qua __proto__ hoặc [[Prototype]]).
  3. Quá trình tiếp tục kiểm tra prototype của prototype, và cứ thế tiếp tục cho đến khi gặp null hoặc tìm thấy thuộc tính/method.

Ưu điểm của Prototype Inheritance

  1. Tiết kiệm bộ nhớ: Các phương thức chung của nhiều đối tượng chỉ được lưu một lần trong prototype. Điều này giúp tối ưu hóa việc sử dụng bộ nhớ.
  2. Chia sẻ phương thức: Các đối tượng có thể chia sẻ các phương thức và thuộc tính thông qua prototype mà không cần phải sao chép chúng cho từng đối tượng riêng lẻ.
  3. Tính mở rộng: Bạn có thể dễ dàng thêm phương thức mới vào prototype của hàm khởi tạo, và tất cả các đối tượng được tạo từ hàm khởi tạo đó đều có thể truy cập vào phương thức mới này.

Nhược điểm của Prototype Inheritance

  1. Khó hiểu cho người mới bắt đầu: Mô hình kế thừa qua prototype có thể khó hiểu hơn so với các ngôn ngữ lập trình hướng đối tượng khác sử dụng lớp (class-based inheritance).
  2. Không có “private”: Các thuộc tính trong prototype có thể được truy cập và sửa đổi từ các đối tượng khác, dẫn đến khả năng bị thay đổi không mong muốn.
  3. Phức tạp khi xử lý các chuỗi prototype dài: Nếu prototype chain quá dài, việc truy cập thuộc tính sẽ tốn nhiều thời gian hơn do phải duyệt qua nhiều prototype.

Tổng kết

Prototype Inheritance trong JavaScript là cơ chế cho phép các đối tượng kế thừa thuộc tính và phương thức từ các đối tượng khác thông qua prototype. Điều này giúp tối ưu hóa bộ nhớ và cho phép chia sẻ phương thức giữa các đối tượng. Tuy nhiên, nó cũng có những nhược điểm liên quan đến tính dễ hiểu và khả năng bảo mật của dữ liệu.