Prototype chain là một khái niệm quan trọng khi bạn tìm hiểu về prototype trong JavaScript. Đọc bài viết này để hiểu prototype chain là gì, tính kế thừa trong prototype chain thể hiện như thế nào. Từ đó, nắm vững lý thuyết và cách áp dụng prototype chain.

Prototype chain là gì?

Trong JavaScript, prototype chain là một cơ chế cho phép các đối tượng truy cập vào các thuộc tính và phương thức được định nghĩa trong các đối tượng prototype của chúng. Mỗi đối tượng trong JavaScript có một liên kết ẩn với prototype của nó.

Nếu một object cố gắng truy cập cùng một thuộc tính có trong hàm constructor và object prototype, thì object đó sẽ lấy thuộc tính từ hàm constructor.

Ví dụ:

function Person() {

    this.name = 'John'

}


// thêm thuộc tính 

Person.prototype.name = 'Peter';

Person.prototype.age = 23


const person1 = new Person();


console.log(person1.name); // John

console.log(person1.age); // 23

Trong chương trình trên, một tên thuộc tính được khai báo trong hàm constructor và cả trong thuộc tính prototype của hàm constructor.

Khi chương trình thực thi, person1.name tìm trong hàm khởi tạo xem có thuộc tính tên name hay không. Vì hàm constructor có thuộc tính tên với giá trị ‘John’, nên object sẽ lấy giá trị từ thuộc tính đó.

Khi chương trình thực thi, person1.age tìm trong hàm khởi tạo để xem có thuộc tính nào tên là age hay không. Vì hàm constructor không có thuộc tính age nên chương trình xem xét object prototype của hàm constructor và object kế thừa thuộc tính từ object prototype (nếu có).

Lưu ý: Bạn cũng có thể truy cập thuộc tính prototype của hàm constructor từ một object.

function Person () {

    this.name = 'John'

}


// thêm 1 prototype

Person.prototype.age = 24;


// tạo object

const person = new Person();


// truy cập thuộc tính prototype

console.log(person.__proto__);   // { age: 24 }

Trong ví dụ trên, một object người được sử dụng để truy cập thuộc tính prototype bằng cách sử dụng __proto__. Tuy nhiên, __proto__ không được dùng nữa nên bạn hãy tránh sử dụng chúng khi code.

Tính kế thừa và prototype chain

Thuộc tính kế thừa

Các object trong JavaScript là các “túi” thuộc tính động (dynamic properties, hoặc thuộc tính riêng – own properties). Chúng liên kết đến một prototype.

Khi bạn muốn truy cập một thuộc tính của một object, trước tiên, quá trình tìm kiếm thuộc tính sẽ bắt đầu từ trong chính object đó. Nếu không tìm thấy thuộc tính đó, sẽ tiếp tục tìm kiếm trong prototype của object. Nếu vẫn không tìm thấy, lại tiếp tục tìm trong prototype của prototype đó. Và quá trình này tiếp tục cho đến khi thuộc tính đó được tìm thấy hoặc cho đến khi hết chuỗi prototype đó (gọi là prototype chain), lúc này sẽ trả về undefined.

Có một số cách để chỉ định [[Prototype]] của một object, sẽ được liệt kê ngay dưới đây. Chúng ta có thể sử dụng cú pháp __proto__ để minh họa.

Một lưu ý rằng cú pháp { __proto__: ... } khác với phương thức getter obj.__proto__ và cú pháp { __proto__: ... } mới là tiêu chuẩn và được khuyến khích dùng.

Trong một object như { a: 1, b: 2, __proto__: c }, giá trị c (phải là null hoặc object khác) sẽ trở thành [[Prototype]] của object được biểu thị bằng chữ, trong khi các key khác như ab sẽ trở thành thuộc tính riêng của object. Cú pháp này đọc rất tự nhiên, vì [[Prototype]] chỉ là một “thuộc tính bên trong”.

Hãy xem ví dụ về truy cập thuộc tính bên dưới:

const o = {

a: 1,

b: 2,

// __proto__ thiết lập [[Prototype]]. Nó được xác định

// như một object khác.

__proto__: {

b: 3,

c: 4,

},

};


// o.[[Prototype]] có thuộc tính b và c.

// o.[[Prototype]].[[Prototype]] là Object.prototype (chúng ta sẽ giải thích

// ý nghĩa sau).

// Cuối cùng, o.[[Prototype]].[[Prototype]].[[Prototype]] là null.

// Đây là kết thúc của prototype chain, là null,

// theo định nghĩa, không sở hữu [[Prototype]].

// Do đó, toàn bộ prototype chain sẽ như sau:

// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null


console.log(o.a); // 1

// Có một thuộc tính 'a' riêng trên o không? Có, giá trị là 1.


console.log(o.b); // 2

// Có một thuộc tính 'b' riêng trên o không? Có, giá trị là 2.

// Prototype cũng có thuộc tính 'b', nhưng nó không được truy cập

// Đây được gọi là Property Shadowing


console.log(o.c); // 4

// Có một thuộc tính 'c' riêng trên o không? Không, kiểm tra prototype.

// Có một thuộc tính 'c' riêng trên o.[[Prototype]không]? Có, giá trị là 4.


console.log(o.d); // không xác định

// Có một thuộc tính 'd' riêng trên o không? Không, kiểm tra prototype.

// Có một thuộc tính 'd' riêng trên o.[[Prototype]không]? Không, kiểm tra prototype.

// o.[[Prototype]].[[Prototype]] là Object.prototype và

// không có thuộc tính 'd' mặc định, kiểm tra prototype.

// o.[[Prototype]].[[Prototype]].[[Prototype]] là null, ngừng tìm kiếm,

// không tìm thấy thuộc tính, trở lại không xác định.

Đặt thuộc tính cho object sẽ tạo thuộc tính riêng. Ngoại lệ duy nhất là khi nó bị chặn bởi một trình thu thập (getter) hoặc trình thiết lập (setter).

Tương tự, bạn có thể tạo nên một prototype chain dài hơn:

const o = {

a: 1,

b: 2,

// __proto__ thiết lập [[Prototype]]. Nó được xác định

// như một object khác.

__proto__: {

b: 3,

c: 4,

__proto__: {

d: 5,

},

},

};




// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null

2. “Phương thức” kế thừa

JavaScript không có “phương thức” nào theo nghĩa “phương thức” mà các ngôn ngữ class-based thường định nghĩa. Mà trong JavaScript, bất kỳ chức năng nào cũng có thể được thêm vào một object dưới dạng thuộc tính.

Một hàm được kế thừa hoạt động giống như bất kỳ thuộc tính nào khác, bao gồm cả hàm properties shadowing như được hiển thị ở trên (trong trường hợp này là một dạng phương thức ghi đè – method overriding). Khi một hàm kế thừa được thực thi, giá trị của this trỏ đến object kế thừa, không phải object prototype trong đó hàm là một thuộc tính riêng.

const parent = {

  value: 2,

  method() {

    return this.value + 1;

  },

};


console.log(parent.method()); // 3

// Khi gọi parent.method trong trường hợp này, 'this' để chỉ parent


// child là một object thừa kế từ parent

const child = {

  __proto__: parent,

};

console.log(child.method()); // 3

// Khi gọi child.method, 'this' để chỉ child.

// Vậy khi child kế thừa phương thức từ parent,

// Thuộc tính 'value' sẽ được tìm trên child. Tuy nhiên, vì child

// không có thuộc tính riêng nào là 'value', thuộc tính sẽ

// được tìm trên [[Prototype]], chính là parent.value.


child.value = 4; // gán giá trị 4 cho thuộc tính 'value' trên child.

// Điều này sẽ shadows thuộc tính 'value' trên parent.

// Object child sẽ như sau:

// { value: 4, __proto__: { value: 2, method: [Function] } }

console.log(child.method()); // 5

// Vì child đã có thuộc tính 'value', 'this.value' sẽ chỉ

// child.value

Tổng kết

Nhìn chung, không phải lúc nào bạn cũng cần phải áp dụng prototype chain khi triển khai JavaScript. Tuy nhiên, điều quan trọng là bạn nên hiểu cách thức hoạt động của prototype chain vì đây là nền tảng cho từ khóa “class” và là kiến thức cần thiết khi bạn có nhu cầu tìm hiểu sâu hơn về ngôn ngữ JavaScript.

robby-2

Bạn thấy bài viết hay và hữu ích? Đừng ngại Share với bạn bè và đồng nghiệp nhé.

Và nhanh tay tham khảo việc làm IT “chất” trên ITviec!