Tại sao Prototype Lại Quan Trọng Trong Javascript

Ở các bài trước, mình đã nói về khái niệm object và this – một số khái niệm cơ bản trong JavaScript. Trong bài này, mình sẽ giải thích khái niệm prototype – một khái niệm khá lòng vòng phức tạp, dễ làm điên đầu các lập trình viên.

Prototype là gì?

Prototype là khái niệm cốt lõi trong JavaScript và là cơ chế quan trọng trong việc thực thi mô hình OOP trong JavaScript. Tất cả các object trong javascript đều có một prototype, và các object này kế thừa các thuộc tính (properties) cũng như phương thức (methods) từ prototype của mình.

Trong JavaScript, trừ undefined, toàn bộ các kiểu còn lại đều là object. Các kiểu string, số, boolean lần lượt là object dạng StringNumberBoolean. Mảng là object dạng Array, hàm là object dạng FunctionPrototype của mỗi object chính là cha của nó, cha của String là String.prototype, cha của Number là Number.prototype, của Array là Array.prototype.

Bạn cần chú ý rằng bản thân prototype là một object trong JS, được gọi là prototype object (đối tượng prototype). Chúng ta cần biết điều này để tránh nhầm lẫn với thuộc tính prototype của function.

Tạo ra prototype như thế nào?

Như đã nói tới ở trên, do hàm khởi tạo đối tượng cũng được xem là 1 đối tượng prototype, do đó các đơn giản để tạo ra 1 đối tượng prototype là khai báo một hàm khởi tạo.

Ví dụ:

//Tạo ra 1 mẫu khởi tạo, cũng là tạo ra 1 prototype object
function infPerson(_age, _name, _address){
   this.age = _age;
   this.name = _name;
   this.address = _address;
}
 
//Có thể thêm thuộc tính vào thuộc tính prototype của hàm khởi tạo
infPerson.prototype.height = 0;
 
//Tạo ra 1 instance của Person
//Có cả 3 thuộc tính của mẫu khởi tạo Person
var person = new infPerson(10, "Khoa", "Da Nang");
for (var att in person){
   console.log(att);
}
 
//Xem đối tượng prototype của instance vừa tạo
person.__proto__;

Đoạn code trên vừa tạo ra một hàm khởi tạo là hàm infPerson(_age, _name, _address), thuộc tính prototype của hàm này lại chứa thuộc tính “height”. Do đó một đối tượng được tạo ra từ hàm khởi tạo này ta sẽ có 4 thuộc tính: age, name, address và height.

Nếu truy cập prototype object của object vừa tạo (instance vừa tạo), thì ta thấy object này là một object chứa 1 hàm khởi tạo và 1 thuộc tính (thuộc tính “height”).

Prototype hoạt động như thế nào?

1. Thêm thuộc tính prototype cho các đối tượng

Chúng ta sẽ cùng tìm hiểu sâu hơn với ví dụ sau đây:

function Person(firstName, lastName) {
    this.firstName = firstName,
    this.lastName = lastName,
    this.fullName = function()  {
        return this.firstName + " " + this.lastName;
    }
}

Hãy khởi tạo 2 object person1 và persion2, sử dụng person constructor như sau:

var person1 = new Person("Khoa", "Nguyen");
var person2 = new Person("Van", "Ho");

Đầu tiên, khi hàm Person được khởi tạo, javascript sẽ thêm thuộc tính prototype vào hàm. Nói cho dễ hiểu là thằng Person sẽ gửi cho constructor 1 cái yêu cầu, nó nói là constructor mày hãy cho tao cái thể hiện đi, constructor hì hục làm việc và trả lại cho nó một cái thể hiện (instance).

Khi ta khởi tạo thêm object person1 bằng hàm constructor: Lúc đối tượng này khởi tạo cũng là lúc javascript engines thêm thuộc tính proto (cũng được gọi là dunder proto) vào đối tượng. Chính dunter proto này sẽ trỏ tới prototype object của hàm constructor.

2. Javascript engines tìm kiếm prototype property như thế nào?

Hãy cùng xem ví dụ sau đây:

// Tạo một hàm constructor function rỗng 
function Person(){
}

// Thêm thuộc tính name, age, address cho prototype property của hàm Person constructor 
Person.prototype.name = "Khoa" ;
Person.prototype.age = 19;
Person.prototype.address = "Da Nang";
Person.prototype.sayName = function(){
	console.log(this.name);
}

// Khởi tạo object sử dụng hàm khởi tạo của Person
var person1 = new Person();

// Truy cập tới thuộc tính name sử dụng đối tượng person
console.log(person1.name) // Output "Khoa"

Khi chúng ta cố gắng truy cập thuộc tính của một đối tượng (ở đây là person1.name), việc đầu tiên javascript engines làm là sẽ cố gắng tìm thuộc tính chúng ta cần trên đối tượng, nếu thuộc tính tồn tại trên đối tượng, như thế thì quá đơn giản, chúng ta chỉ việc xuất ra kết quả.

Nếu không, lúc này nó sẽ kiểm tra thuộc tính ở đối tượng nguyên mẫu (prototype object) hoặc đối tượng mà nó kế thừa. Trường hợp tệ nhất, đến cuối cùng vẫn không tìm được thuộc tính -> kết quả trả về sẽ là undefined.

Đối với ví dụ trên, khi person1.name được gọi, javascript engines sẽ kiểm tra property này có tồn tại trên đối tượng Person hay không?. Không may thay, trường hợp này thuộc tính name không tồn tại trên đối tượng person, nên nó sẽ tiếp tục tìm kiếm trên dunder proto hoặc trên prototype của đối tượng person. Rất may thuộc tính name tồn tại trên prototype của đối tượng person, nên nó sẽ cho kết quả là Khoa.

Điều gì tạo nên sự quan trọng của Prototype?

1. Cơ chế kế thừa trong Javascript

Trong JavaScript, việc kế thừa được hiện thực thông qua prototype theo cơ chế prototype-based. Ngắn gọn hơn, để thực hiện kế thừa trong Javascript, bạn cần tạo 1 hàm khởi tạo, sau đó thêm các thuộc tính và phương thức vào thuộc tính prototype của hàm khởi tạo này. Các instance tạo ra bởi hàm khởi tạo này sẽ chứa các thuộc tính và phương thức được định nghĩa ở trên.
Hãy xem ví dụ dưới đây để hiểu rõ hơn:

//Tạo ra 1 hàm khởi tạo cơ sở (tựa như lớp cơ sở)
function Person(_age, _name){
   this.age = _age;
   this.name = _name;
 
//Có thể thêm thuộc tính vào thuộc tính prototype của hàm khởi tạo
Person.prototype.showAge = function(){
   console.log( this.age );
};
 
//Tạo ra 1 hàm khởi tạo con (sẽ dùng để kế thừa lớp cơ sở)
function Student(_id, _score){
   this.id = _id;
   this.score = _score;
}
//Thực hiện kế thừa, gán hàm khởi tạo của Animal cho prototype của Bird
Student.prototype = new Person(19, 'Khoa');
Student.prototype.showScore = function(){
   console.log( this.score );
};
 
//Kiểm tra sự kế thừa
var student1 = new Student(01, 100);
student1.age = 19;
student1.showAge();       //19
student1.showScore();     //100

Ở ví dụ trên, đối tượng student1 sử dụng được hàm showAge() của Person prototype bởi vì ta đã gán hàm khởi tạo của Person vào prototype của Student.

Đây chính là cơ chế kế thừa trong Javascript, đối tượng student1 đã kế thừa những gì có trong prototype của nó (là Student.prototype), và nó cũng kế thừa luôn những gì có trong thuộc tính prototype của Student (chính là Person.prototype).

2. Prototype chain

Prototype rất quan trọng trong việc giúp ta truy cập tới các thuộc tính và phương thức của đối tượng. Đặc tính prototype của đối tượng (hay còn gọi là prototype object) là một “object cha” nơi chứa các thuộc tính và phương thức được kế thừa. Vì thế, khi ta gọi tới một thuộc tính của đối tượng (vd: student1.age), ban đầu Js sẽ tìm trong thuộc tính riêng của đối tượng, nếu không tìm thấy, nó sẽ tiếp tục tìm trong prototype của đối tượng, và lặp lại tiếp với prototype của đối tượng prototype, … Quá trình lặp lại này được gọi là chuỗi prototype trong Javascript. Chính điều này + thuộc tính prototype của function tạo nên cơ chế kế thừa prototype-based cho Javascript.

Ví dụ:

var obj1 = { a: 1 };
var obj2 = Object.create(obj1);
obj2.b = 2;
console.log(obj1.a); // 1
console.log(obj2.a); // 1
console.log(obj2.b); // 2
console.log(obj2.c); // undefined

Trong ví dụ trên, Object.create() sẽ tạo một object mới obj2 với prototype là obj1. Và như đã thấy, mặc dù obj2 không có property a, nhưng chúng ta vẫn có thể truy cập nó nhờ vào cơ chế prototype chain.

Tạm kết

Như vậy trong bài viết này, chúng ta đã cùng tìm hiểu về prototype trong javascript. Bạn thấy thế nào về JS, hãy đưa ra những ý kiến trong quá trình sử dụng js nhé. Nếu các bạn thấy bài viết hữu ích hãy rate 5* và share cho mọi người tham khảo!

Leave a reply:

Your email address will not be published.

Site Footer

Sliding Sidebar

Facebook