Prototype trong JavaScript là một khái niệm quan trọng và cơ bản, giúp hiểu rõ cách thức hoạt động của hệ thống kế thừa và chia sẻ phương thức giữa các đối tượng. Trong ngôn ngữ lập trình này, mọi đối tượng đều có một prototype, và khi không tìm thấy thuộc tính hoặc phương thức trong đối tượng, JavaScript sẽ tìm kiếm trong chuỗi nguyên mẫu (prototype chain). Việc nắm vững cách thức hoạt động của prototype giúp bạn tạo ra mã nguồn hiệu quả, tối ưu hóa bộ nhớ và dễ dàng mở rộng tính năng cho các dự án phức tạp.

1. Hiểu về Prototype trong JavaScript

Mỗi đối tượng trong JavaScript đều có một thuộc tính gọi là [[Prototype]], thường được truy cập thông qua thuộc tính __proto__ hoặc qua từ khóa prototype đối với các hàm tạo (constructor). Khi bạn cố gắng truy cập một thuộc tính hoặc phương thức của một đối tượng, nếu thuộc tính đó không tồn tại trong đối tượng, JavaScript sẽ tra cứu trong chuỗi nguyên mẫu (prototype chain) để tìm thuộc tính đó.

Ví dụ:

let obj = { name: "Anh" };
console.log(obj.hasOwnProperty('name')); // true
console.log(obj.hasOwnProperty('toString')); // false
console.log(obj.toString()); // "[object Object]"

Trong ví dụ này, toString() không phải là phương thức của đối tượng obj, nhưng JavaScript đã tìm thấy phương thức này thông qua prototype của đối tượng Object.

2. Cách thức hoạt động của Prototype

Khi một đối tượng mới được tạo từ một hàm tạo (constructor function), nó sẽ “thừa kế” các phương thức và thuộc tính từ prototype của hàm tạo đó. Điều này cho phép các đối tượng chia sẻ phương thức và thuộc tính chung mà không cần phải lưu trữ chúng nhiều lần trong bộ nhớ.

Ví dụ:

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.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

let person1 = new Person("Anh", 30);
let person2 = new Person("Em", 25);

person1.greet(); // "Hello, my name is Anh"
person2.greet(); // "Hello, my name is Em"

Ở đây, cả person1person2 đều sử dụng phương thức greet() thông qua prototype của Person. Điều này giúp tiết kiệm bộ nhớ vì phương thức greet không được sao chép nhiều lần cho từng đối tượng, mà chỉ tồn tại một lần trong prototype.

3. Prototype Chain (Chuỗi nguyên mẫu)

Khi bạn truy cập một thuộc tính hoặc phương thức trên một đối tượng, JavaScript sẽ tìm kiếm trong đối tượng đó trước. Nếu không tìm thấy, nó sẽ tiếp tục tìm kiếm trong prototype của đối tượng. Quá trình này lặp lại cho đến khi đến đối tượng gốc (Object.prototype) hoặc khi tìm thấy thuộc tính/phương thức đó.

Ví dụ:

let arr = [1, 2, 3];
console.log(arr.hasOwnProperty('length')); // true
console.log(arr.toString()); // "1,2,3"
console.log(arr.hasOwnProperty('toString')); // false

Trong ví dụ này, phương thức toString() không có trong đối tượng arr, nhưng nó được tìm thấy trong Array.prototype, sau đó là Object.prototype.

4. Prototype và Kế thừa (Inheritance)

Prototype cho phép JavaScript thực hiện kế thừa. Bạn có thể tạo một lớp con kế thừa từ lớp cha thông qua việc liên kết prototype của chúng.

Ví dụ về kế thừa:

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

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound`);
};

function Dog(name, breed) {
  Animal.call(this, name); // Gọi hàm tạo của lớp cha
  this.breed = breed;
}

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

Dog.prototype.bark = function() {
  console.log(`${this.name} barks`);
};

let dog = new Dog("Rex", "German Shepherd");
dog.speak(); // "Rex makes a sound"
dog.bark();  // "Rex barks"

Trong ví dụ này, Dog kế thừa từ Animal, vì vậy Dog có thể sử dụng phương thức speak() của Animal thông qua cơ chế prototype.

5. Kế thừa nhiều cấp (Multilevel Inheritance)

JavaScript hỗ trợ kế thừa nhiều cấp thông qua chuỗi prototype. Điều này có nghĩa là một đối tượng có thể kế thừa từ một đối tượng khác, và đối tượng đó lại có thể kế thừa từ một đối tượng khác nữa.

Ví dụ về kế thừa nhiều cấp:

function GrandParent() {
  this.grandParentProp = "GrandParent Property";
}

GrandParent.prototype.showGrandParentProp = function() {
  console.log(this.grandParentProp);
};

function Parent() {
  GrandParent.call(this); // Kế thừa thuộc tính từ GrandParent
  this.parentProp = "Parent Property";
}

Parent.prototype = Object.create(GrandParent.prototype);
Parent.prototype.constructor = Parent;

function Child() {
  Parent.call(this); // Kế thừa thuộc tính từ Parent
  this.childProp = "Child Property";
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

let child = new Child();
child.showGrandParentProp(); // "GrandParent Property"
console.log(child.parentProp); // "Parent Property"
console.log(child.childProp);  // "Child Property"

Trong ví dụ này, Child kế thừa từ Parent, và Parent kế thừa từ GrandParent. Điều này cho phép Child truy cập các thuộc tính và phương thức của cả ParentGrandParent.

6. Những hạn chế của Prototype

Mặc dù cơ chế prototype trong JavaScript rất mạnh mẽ, nhưng nó cũng có một số hạn chế:

  • Tính phức tạp: Khi chuỗi prototype quá dài, việc theo dõi thuộc tính và phương thức trở nên phức tạp, dễ gây nhầm lẫn.
  • Thiếu hỗ trợ cho kế thừa đa hình (Multiple Inheritance): JavaScript không hỗ trợ kế thừa từ nhiều lớp cùng một lúc.
  • Lỗi do ghi đè: Việc ghi đè các phương thức trong prototype có thể gây ra lỗi nếu không cẩn thận.

7. ES6 Class và Prototype

Trong phiên bản ECMAScript 6 (ES6), JavaScript giới thiệu cú pháp class để làm cho việc làm việc với prototype trở nên đơn giản và trực quan hơn. Mặc dù cú pháp thay đổi, bản chất vẫn dựa trên cơ chế prototype.

Ví dụ sử dụng class trong ES6:

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

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

let person1 = new Person("Anh", 30);
person1.greet(); // "Hello, my name is Anh"

Dù cú pháp class dễ hiểu hơn, nhưng các phương thức trong class vẫn được lưu trữ trong prototype của đối tượng.

Kết luận

Prototype là nền tảng của hệ thống kế thừa trong JavaScript, cho phép các đối tượng chia sẻ các phương thức và thuộc tính. Hiểu rõ cách hoạt động của prototype giúp bạn tạo ra mã nguồn hiệu quả, tái sử dụng, và tối ưu hóa bộ nhớ trong các dự án phức tạp. Dù ES6 đã giới thiệu cú pháp class, bản chất của kế thừa trong JavaScript vẫn dựa trên cơ chế prototype.