Prototype Chain
Chuỗi nguyên mẫu - Prototype Chain
ECMAScript mô tả khái niệm về chuỗi nguyên mẫu và sử dụng chuỗi nguyên mẫu là phương pháp chính để thực hiện kế thừa. Ý tưởng cơ bản là sử dụng nguyên mẫu để cho một kiểu tham chiếu kế thừa các thuộc tính và phương thức từ một kiểu tham chiếu khác.
Để hiểu rõ về đối tượng nguyên mẫu và chuỗi nguyên mẫu, chúng ta cần hiểu mối quan hệ giữa prototype
, __proto__
và constructor
.
Chúng ta sẽ giải thích mối quan hệ phức tạp này thông qua ví dụ và hình ảnh minh họa.
Hình ảnh minh họa
- Mũi tên màu đỏ biểu thị sự chỉ định của thuộc tính
__proto__
- Mũi tên màu xanh lá cây biểu thị sự chỉ định của thuộc tính
prototype
- Mũi tên màu nâu biểu thị thuộc tính
constructor
của chính đối tượng - Hình vuông màu xanh da trời biểu thị đối tượng
- Hình vuông màu xanh lá cây nhạt biểu thị hàm
Trong JavaScript, ba thành phần này đính kèm vào các loại đối tượng khác nhau.
- Đối tượng:
__proto__
vàconstructor
chỉ thuộc về đối tượng. - Hàm:
prototype
chỉ thuộc về hàm. Nhưng hàm cũng là đối tượng, nên hàm cũng có__proto__
vàconstructor
.
Nguyên mẫu rõ ràng - prototype
Đối tượng nguyên mẫu rõ ràng prototype
là duy nhất cho mỗi hàm và nó trỏ từ một hàm đến một đối tượng khác. Nó đại diện cho đối tượng nguyên mẫu của hàm này (thực chất, tất cả các hàm đều có thể được sử dụng như là hàm tạo). Do đó, foo.__proto__ === Foo.prototype
và chúng hoàn toàn giống nhau.
Vậy tác dụng của thuộc tính prototype
là gì? Tác dụng của nó là chứa các thuộc tính và phương thức được tất cả các thể hiện của kiểu cụ thể chia sẻ, cho phép các đối tượng được tạo bởi hàm này (thực chất là tất cả các hàm) có thể truy cập vào các thuộc tính và phương thức chung.
Khi tạo một hàm, một đối tượng prototype
của hàm cũng được tạo mặc định.
Nguyên mẫu ẩn - __proto__
Trong JavaScript, mọi đối tượng đều có một thuộc tính __proto__
, như được thể hiện trong hình ảnh trên. Một đối tượng trỏ đến một đối tượng khác thông qua thuộc tính __proto__
, nghĩa là nó trỏ đến đối tượng nguyên mẫu tương ứng. Đối tượng nguyên mẫu này được gọi là nguyên mẫu ẩn.
Nguyên mẫu ẩn có tác dụng là khi truy cập vào 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 đó, nó sẽ tìm kiếm trong đối tượng nguyên mẫu (đối tượng nguyên mẫu cũng là một đối tượng và cũng có nguyên mẫu riêng của nó), nếu không tìm thấy, nó sẽ tiếp tục tìm kiếm trong nguyên mẫu của đối tượng nguyên mẫu đó, và cứ tiếp tục như vậy cho đến khi tìm thấy thuộc tính hoặc phương thức, hoặc tìm thấy nguyên mẫu đầu tiên null
, quá trình tìm kiếm kết thúc và trả về undefined
.
Trong quá trình tìm kiếm này, từ đối tượng hiện tại, nó sẽ tiếp tục tìm kiếm các thuộc tính và phương thức liên quan thông qua chuỗi nguyên mẫu (nguyên mẫu cũng có nguyên mẫu riêng của nó). Tất cả các đối tượng liên kết này tạo thành một chuỗi gọi là chuỗi nguyên mẫu.
Hàm tạo - Constrcutor
Thuộc tính constructor
cũng chỉ thuộc về đối tượng và nó trỏ từ một đối tượng đến một hàm. Ý nghĩa của nó là hàm tạo của đối tượng đó và mỗi đối tượng đều có một hàm tạo (có thể là riêng của nó hoặc được kế thừa, để rõ hơn, hãy xem thuộc tính __proto__
). Từ hình ảnh trên, ta có thể thấy rằng đối tượng đặc biệt Function
có một hàm tạo riêng của nó (vì Function
có thể được coi là một hàm hoặc một đối tượng), tất cả các hàm và đối tượng đều được tạo ra từ hàm tạo Function
, do đó thuộc tính constructor
cuối cùng là hàm Function
.
Đối tượng nguyên mẫu
Đối tượng nguyên mẫu là đối tượng cha của đối tượng hiện tại.
Nguyên mẫu rõ ràng | Nguyên mẫu ẩn |
---|---|
Thuộc tính prototype | Thuộc tính __proto__ |
Riêng của hàm | Riêng của đối tượng (đối tượng cũng có thuộc tính __proto__ và constructor ) |
Được gán mặc định khi định nghĩa hàm | Được thêm tự động khi tạo đối tượng, và được gán giá trị của prototype của hàm tạo |
Dùng để thực hiện kế thừa dựa trên nguyên mẫu và chia sẻ thuộc tính | Tạo thành chuỗi nguyên mẫu và dùng để thực hiện kế thừa dựa trên nguyên mẫu |
🌰 Ví dụ mã: Truy cập đối tượng nguyên mẫu trong chuỗi nguyên mẫu
const Foo = function () {};
const foo = new Foo();
// Đối tượng tạo Foo {} là đối tượng nguyên mẫu của hàm foo
console.log(foo.__proto__);
// Đối tượng tạo Object {} là đối tượng nguyên mẫu của hàm Function
console.log(foo.__proto__.__proto__);
// Đầu chuỗi nguyên mẫu
console.log(foo.__proto__.__proto__.__proto__);
// null
Sự khác biệt giữa tìm kiếm thuộc tính/phương thức trong chuỗi nguyên mẫu và truy cập đối tượng nguyên mẫu trong chuỗi nguyên mẫu
- Khi tìm kiếm thuộc tính hoặc phương thức trong chuỗi nguyên mẫu, nếu không tìm thấy thuộc tính hoặc phương thức tương ứng, kết quả trả về là
undefined
, đồng nghĩa với việc không có thuộc tính hoặc phương thức trong chuỗi nguyên mẫu. - Khi truy cập đối tượng nguyên mẫu trong chuỗi nguyên mẫu, đến cuối chuỗi nguyên mẫu, tức là
Object.prototype
, giá trị của nó lànull
.
Sự chỉ định của đối tượng nguyên mẫu
__proto__
chỉ định của đối tượng nguyên mẫu phụ thuộc vào cách tạo đối tượng.
Tạo đối tượng bằng cách sử dụng Literal
Khi tạo đối tượng bằng cách sử dụng literal, nguyên mẫu của nó là Object.prototype
.
Mặc dù chúng ta không thể truy cập trực tiếp vào thuộc tính __proto__
được nhúng sẵn, nhưng chúng ta có thể sử dụng Object.getPrototypeOf()
hoặc thuộc tính __proto__
của đối tượng để truy cập nguyên mẫu của đối tượng.
const foo = {};
console.log(foo.__proto__ === Object.prototype);
// true
console.log(Object.getPrototypeOf(foo) === Object.prototype);
// true
Tạo đối tượng bằng cách sử dụng hàm tạo
const Foo = function () {};
const foo = new Foo();
console.log(foo.__proto__ === Foo.prototype);
// true
console.log(Object.getPrototypeOf(foo) === Foo.prototype);
// true
Tạo đối tượng bằng cách sử dụng Object.create()
Đối tượng được tạo bằng cách sử dụng Object.create()
sẽ có nguyên mẫu là đối tượng được truyền vào làm đối số.
const foo = {};
const bar = Object.create(foo);
console.log(bar.__proto__ === foo);
Nguyên mẫu đối tượng và thể hiện
Chúng ta có thể xác định mối quan hệ giữa nguyên mẫu và thể hiện thông qua toán tử instanceof
.
// Giả sử toán tử instanceof có dạng L instanceof R
L instanceof R
// Toán tử instanceof kiểm tra xem R.prototype có tồn tại trong chuỗi nguyên mẫu của L hay không
L.__proto__.__proto__... === R.prototype
⚠️ Lưu ý: Toán tử instanceof sẽ kiểm tra đệ quy chuỗi nguyên mẫu của L, tức là L.__proto__.__proto__.__proto__…
cho đến khi tìm thấy hoặc đạt đến nguyên mẫu đầu tiên làm kết thúc.
Hàm tạo Function có hàm tạo là chính nó:
Function.constructor === Function; // true
Hàm tạo Object có hàm tạo là Function (do đó, tất cả các hàm tạo đều trỏ đến Function)
Object.constructor === Function; // true
[[Prototype]]
của hàm tạo Function là một hàm vô danh đặc biệt
console.log(Function.__proto__); // function(){}
Hàm vô danh đặc biệt này trỏ đến nguyên mẫu của Object.prototype
.
Function.__proto__.__proto__ === Object.prototype; // true
[[Prototype]]
của Object trỏ đến nguyên mẫu của Function, tức là hàm vô danh đặc biệt đã được đề cập ở trên.
Object.__proto__ === Function.prototype; // true
Function.prototype === Function.__proto__; // true
Mối quan hệ giữa hàm tạo Function và Object:
Function instanceof Object; // true
Object instanceof Function; // true
Mối quan hệ chuỗi nguyên mẫu của các đối tượng được tạo bằng từ khóa:
console.log(true.__proto__.__proto__ === Object.prototype);
console.log((123).__proto__.__proto__ === Object.prototype);
console.log('String'.__proto__.__proto__ === Object.prototype);
console.log([].__proto__.__proto__ === Object.prototype);
console.log({}.__proto__ === Object.prototype);
Tóm tắt:
- Thuộc tính constructor của tất cả các hàm tạo đều trỏ đến Function.
- Thuộc tính prototype của Function trỏ đến một hàm vô danh đặc biệt, và hàm vô danh đặc biệt này trỏ đến Object.prototype.