Thứ sáu, 12/01/2018 | 00:00 GMT+7

Tìm hiểu Nguyên mẫu và Kế thừa trong JavaScript

JavaScript là một ngôn ngữ dựa trên nguyên mẫu , nghĩa là các thuộc tính và phương thức của đối tượng có thể được chia sẻ thông qua các đối tượng tổng quát có khả năng được nhân bản và mở rộng. Đây được gọi là kế thừa nguyên mẫu và khác với kế thừa lớp. Trong số các ngôn ngữ lập trình hướng đối tượng phổ biến, JavaScript tương đối độc đáo, vì các ngôn ngữ nổi bật khác như PHP, Python và Java là các ngôn ngữ dựa trên lớp, thay vào đó định nghĩa các lớp là bản thiết kế cho các đối tượng.

Trong hướng dẫn này, ta sẽ tìm hiểu nguyên mẫu đối tượng là gì và cách sử dụng hàm khởi tạo để mở rộng nguyên mẫu thành đối tượng mới. Ta cũng sẽ tìm hiểu về kế thừa và chuỗi nguyên mẫu.

Nguyên mẫu JavaScript

Trong phần Tìm hiểu đối tượng trong JavaScript , ta đã xem qua kiểu dữ liệu đối tượng, cách tạo đối tượng và cách truy cập và sửa đổi các thuộc tính đối tượng. Bây giờ ta sẽ tìm hiểu cách các nguyên mẫu được dùng để mở rộng các đối tượng.

Mọi đối tượng trong JavaScript đều có một thuộc tính bên trong được gọi là [[Prototype]] . Ta có thể chứng minh điều này bằng cách tạo một đối tượng mới, trống.

let x = {}; 

Đây là cách ta thường tạo một đối tượng, nhưng lưu ý một cách khác để thực hiện điều này là với hàm tạo đối tượng: let x = new Object() .

Dấu ngoặc vuông kép bao quanh [[Prototype]] biểu thị rằng nó là một thuộc tính nội bộ và không thể được truy cập trực tiếp trong mã.

Để tìm [[Prototype]] của đối tượng mới được tạo này, ta sẽ sử dụng phương thức getPrototypeOf() .

Object.getPrototypeOf(x); 

Đầu ra sẽ bao gồm một số thuộc tính và phương thức được tích hợp sẵn.

Output
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

Một cách khác để tìm [[Prototype]] là thông qua thuộc tính __proto__ . __proto__ là một thuộc tính thể hiện [[Prototype]] trong của một đối tượng.

Điều quan trọng cần lưu ý là .__proto__ là một tính năng kế thừa và không nên được sử dụng trong mã production và nó không có trong mọi trình duyệt hiện đại. Tuy nhiên, ta có thể sử dụng nó trong suốt bài viết này cho các mục đích minh họa.

x.__proto__; 

Đầu ra sẽ giống như khi bạn đã sử dụng getPrototypeOf() .

Output
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

Điều quan trọng là mọi đối tượng trong JavaScript đều có [[Prototype]] vì nó tạo ra một cách để hai hoặc nhiều đối tượng bất kỳ được liên kết.

Các đối tượng mà bạn tạo có [[Prototype]] , cũng như các đối tượng tích hợp, chẳng hạn như DateArray . Một tham chiếu có thể được thực hiện đến thuộc tính nội bộ này từ đối tượng này sang đối tượng khác thông qua thuộc tính prototype , như ta sẽ thấy ở phần sau của hướng dẫn này.

Kế thừa nguyên mẫu

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, trước tiên JavaScript sẽ tìm kiếm trên chính đối tượng đó và nếu không tìm thấy nó, nó sẽ tìm kiếm [[Prototype]] của đối tượng. Nếu sau khi tham khảo cả đối tượng và [[Prototype]] mà vẫn không tìm thấy đối tượng nào phù hợp, JavaScript sẽ kiểm tra nguyên mẫu của đối tượng được liên kết và tiếp tục tìm kiếm cho đến khi đạt đến cuối chuỗi nguyên mẫu.

Ở cuối chuỗi nguyên mẫu là Object.prototype . Tất cả các đối tượng đều kế thừa các thuộc tính và phương thức của Object . Bất kỳ nỗ lực nào để tìm kiếm ngoài phần cuối của chuỗi đều dẫn đến kết quả là null .

Trong ví dụ của ta , x là một đối tượng rỗng kế thừa từ Object . x có thể sử dụng bất kỳ thuộc tính hoặc phương thức nào mà Object có, chẳng hạn như toString() .

x.toString(); 
Output
[object Object]

Chuỗi nguyên mẫu này chỉ dài một mắt xích. x -> Object . Ta biết điều này, bởi vì nếu ta cố gắng liên kết hai thuộc tính [[Prototype]] với nhau, nó sẽ là null .

x.__proto__.__proto__; 
Output
null

Hãy xem xét một loại đối tượng khác. Nếu bạn có kinh nghiệm Làm việc với Mảng trong JavaScript , bạn biết chúng có nhiều phương thức tích hợp, chẳng hạn như pop()push() . Lý do bạn có quyền truy cập vào các phương thức này khi tạo một mảng mới là vì bất kỳ mảng nào bạn tạo đều có quyền truy cập vào các thuộc tính và phương thức trên Array.prototype .

Ta có thể kiểm tra bằng cách tạo một mảng mới.

let y = []; 

Lưu ý ta cũng có thể viết nó như một hàm tạo mảng, let y = new Array() .

Nếu ta nhìn vào [[Prototype]] của mảng y mới, ta sẽ thấy rằng nó có nhiều thuộc tính và phương thức hơn đối tượng x . Nó đã kế thừa mọi thứ từ Array.prototype .

y.__proto__; 
[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …] 

Bạn sẽ nhận thấy một thuộc tính phương thức constructor trên nguyên mẫu được đặt thành Array() . Các constructor bất động sản trả về hàm xây dựng của một đối tượng, mà là một cơ chế sử dụng các đối tượng xây dựng từ chức năng.

Bây giờ ta có thể xâu chuỗi hai nguyên mẫu lại với nhau, vì trong trường hợp này, chuỗi nguyên mẫu của ta dài hơn. Nó trông giống như y -> Array -> Object .

y.__proto__.__proto__; 
Output
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

Chuỗi này hiện đang tham chiếu đến Object.prototype . Ta có thể kiểm tra nội bộ [[Prototype]] đối với thuộc tính prototype của hàm khởi tạo để thấy rằng chúng đang đề cập đến cùng một thứ.

y.__proto__ === Array.prototype;            // true y.__proto__.__proto__ === Object.prototype; // true 

Ta cũng có thể sử dụng phương thức isPrototypeOf() để thực hiện điều này.

Array.prototype.isPrototypeOf(y);      // true Object.prototype.isPrototypeOf(Array); // true 

Ta có thể sử dụng toán tử instanceof để kiểm tra xem thuộc tính prototype của một hàm tạo có xuất hiện ở bất kỳ đâu trong chuỗi nguyên mẫu của đối tượng hay không.

y instanceof Array; // true 

Tóm lại, tất cả các đối tượng JavaScript đều có thuộc tính [[Prototype]] ẩn, bên trong (có thể được hiển thị thông qua __proto__ trong một số trình duyệt). Các đối tượng có thể được mở rộng và sẽ kế thừa các thuộc tính và phương thức trên [[Prototype]] của phương thức khởi tạo của chúng.

Các nguyên mẫu này có thể được xâu chuỗi và mỗi đối tượng bổ sung sẽ kế thừa mọi thứ trong toàn bộ chuỗi. Chuỗi kết thúc bằng Object.prototype .

Hàm tạo

Hàm tạo là các hàm được sử dụng để xây dựng các đối tượng mới. Toán tử new được sử dụng để tạo các version mới dựa trên một hàm khởi tạo. Ta đã thấy một số hàm tạo JavaScript tích hợp sẵn, chẳng hạn như new Array()new Date() , nhưng ta cũng có thể tạo các mẫu tùy chỉnh của riêng mình để từ đó xây dựng các đối tượng mới.

Ví dụ, giả sử ta đang tạo một trò chơi nhập vai dựa trên văn bản, rất đơn giản. User có thể chọn một nhân vật và sau đó chọn lớp nhân vật mà họ sẽ có, chẳng hạn như chiến binh, người chữa bệnh, kẻ trộm, v.v.

Vì mỗi nhân vật sẽ chia sẻ nhiều đặc điểm, chẳng hạn như có tên, cấp độ và điểm đánh, nên việc tạo một hàm tạo làm mẫu là rất hợp lý. Tuy nhiên, vì mỗi lớp nhân vật có thể có những khả năng khác nhau rất lớn, ta muốn đảm bảo mỗi nhân vật chỉ có khả năng riêng của họ. Ta hãy xem cách ta có thể thực hiện điều này với kế thừa nguyên mẫu và các hàm tạo.

Để bắt đầu, một hàm khởi tạo chỉ là một hàm thông thường. Nó trở thành một phương thức khởi tạo khi nó được gọi bởi một thể hiện với từ khóa new . Trong JavaScript, ta viết hoa chữ cái đầu tiên của một hàm tạo theo quy ước.

characterSelect.js
// Initialize a constructor function for a new Hero function Hero(name, level) {   this.name = name;   this.level = level; } 

Ta đã tạo một hàm khởi tạo có tên là Hero với hai tham số: namelevel . Vì mọi nhân vật sẽ có tên và cấp độ, nên việc mỗi nhân vật mới có những thuộc tính này sẽ rất hợp lý. Từ khóa this sẽ tham chiếu đến version mới được tạo, vì vậy việc đặt this.name thành tham số name đảm bảo đối tượng mới sẽ có một bộ thuộc tính name .

Bây giờ ta có thể tạo một thể hiện new với new .

let hero1 = new Hero('Bjorn', 1); 

Nếu ta điều khiển hero1 , ta sẽ thấy một đối tượng mới đã được tạo với các thuộc tính mới được đặt như mong đợi.

Output
Hero {name: "Bjorn", level: 1}

Bây giờ nếu ta nhận được [[Prototype]] của hero1 , ta sẽ có thể thấy hàm constructorHero() . ( Lưu ý điều này có cùng đầu vào như hero1.__proto__ , nhưng là phương pháp thích hợp để sử dụng.)

Object.getPrototypeOf(hero1); 
Output
constructor: ƒ Hero(name, level)

Bạn có thể nhận thấy rằng ta chỉ xác định các thuộc tính chứ không phải các phương thức trong hàm tạo. Một thực tế phổ biến trong JavaScript là xác định các phương thức trên nguyên mẫu để tăng hiệu quả và khả năng đọc mã.

Ta có thể thêm một phương thức vào Hero bằng cách sử dụng prototype . Ta sẽ tạo một phương thức greet() .

characterSelect.js
... // Add greet method to the Hero prototype Hero.prototype.greet = function () {   return `${this.name} says hello.`; } 

hero1 greet() nằm trong prototype của Herohero1 là một version của Hero , nên phương thức này có sẵn cho hero1 .

hero1.greet(); 
Output
"Bjorn says hello."

Nếu bạn kiểm tra [[Prototype]] của Anh hùng, bạn sẽ thấy greet() là một tùy chọn khả dụng ngay bây giờ.

Điều này là tốt, nhưng bây giờ ta muốn tạo các lớp nhân vật cho các anh hùng sử dụng. Sẽ không hợp lý nếu đặt tất cả các khả năng của mọi lớp vào hàm tạo Hero , vì các lớp khác nhau sẽ có các khả năng khác nhau. Ta muốn tạo các hàm khởi tạo mới, nhưng ta cũng muốn chúng được kết nối với Hero root .

Ta có thể sử dụng phương thức call() để sao chép các thuộc tính từ một hàm tạo này sang một hàm tạo khác. Hãy tạo một Warrior và một nhà xây dựng Healer.

characterSelect.js
... // Initialize Warrior constructor function Warrior(name, level, weapon) {   // Chain constructor with call   Hero.call(this, name, level);    // Add a new property   this.weapon = weapon; }  // Initialize Healer constructor function Healer(name, level, spell) {   Hero.call(this, name, level);    this.spell = spell; } 

Cả hai cấu tạo mới hiện đều có các thuộc tính của Hero và một số cấu tạo không có tính chất khác. Ta sẽ thêm phương thức attack() vào Warrior và phương thức heal() cho Healer .

characterSelect.js
... Warrior.prototype.attack = function () {   return `${this.name} attacks with the ${this.weapon}.`; }  Healer.prototype.heal = function () {   return `${this.name} casts ${this.spell}.`; } 

Đến đây, ta sẽ tạo các nhân vật của bạn với hai lớp nhân vật mới có sẵn.

characterSelect.js
const hero1 = new Warrior('Bjorn', 1, 'axe'); const hero2 = new Healer('Kanin', 1, 'cure'); 

hero1 hiện được công nhận là Warrior với các thuộc tính mới.

Output
Warrior {name: "Bjorn", level: 1, weapon: "axe"}

Ta có thể sử dụng các phương pháp mới mà ta đã cài đặt trên nguyên mẫu Warrior .

hero1.attack(); 
Console
"Bjorn attacks with the axe."

Nhưng điều gì sẽ xảy ra nếu ta cố gắng sử dụng các phương pháp sâu hơn trong chuỗi nguyên mẫu?

hero1.greet(); 
Output
Uncaught TypeError: hero1.greet is not a function

Các thuộc tính và phương thức nguyên mẫu không được tự động liên kết khi bạn sử dụng hàm call() để tạo chuỗi. Ta sẽ sử dụng Object.create() để liên kết các nguyên mẫu, đảm bảo đặt nó trước khi bất kỳ phương thức bổ sung nào được tạo và thêm vào nguyên mẫu.

characterSelect.js
... Warrior.prototype = Object.create(Hero.prototype); Healer.prototype = Object.create(Hero.prototype);  // All other prototype methods added below ... 

Như vậy, ta có thể sử dụng thành công các phương thức nguyên mẫu từ Hero trên một version của Warrior hoặc Healer .

hero1.greet(); 
Output
"Bjorn says hello."

Đây là mã đầy đủ cho trang tạo nhân vật của ta .

characterSelect.js
// Initialize constructor functions function Hero(name, level) {   this.name = name;   this.level = level; }  function Warrior(name, level, weapon) {   Hero.call(this, name, level);    this.weapon = weapon; }  function Healer(name, level, spell) {   Hero.call(this, name, level);    this.spell = spell; }  // Link prototypes and add prototype methods Warrior.prototype = Object.create(Hero.prototype); Healer.prototype = Object.create(Hero.prototype);  Hero.prototype.greet = function () {   return `${this.name} says hello.`; }  Warrior.prototype.attack = function () {   return `${this.name} attacks with the ${this.weapon}.`; }  Healer.prototype.heal = function () {   return `${this.name} casts ${this.spell}.`; }  // Initialize individual character instances const hero1 = new Warrior('Bjorn', 1, 'axe'); const hero2 = new Healer('Kanin', 1, 'cure'); 

Với mã này, ta đã tạo lớp Hero mình với các thuộc tính cơ sở, tạo hai lớp nhân vật có tên là WarriorHealer từ phương thức khởi tạo ban đầu, thêm các phương thức vào nguyên mẫu và tạo các cá thể nhân vật riêng lẻ.

Kết luận

JavaScript là một ngôn ngữ dựa trên nguyên mẫu và hoạt động khác với mô hình dựa trên lớp truyền thống mà nhiều ngôn ngữ hướng đối tượng khác sử dụng.

Trong hướng dẫn này, ta đã tìm hiểu cách thức hoạt động của các nguyên mẫu trong JavaScript và cách liên kết các thuộc tính và phương thức của đối tượng thông qua thuộc tính [[Prototype]] mà tất cả các đối tượng dùng chung. Ta cũng đã học cách tạo các hàm khởi tạo tùy chỉnh và cách hoạt động của kế thừa nguyên mẫu để chuyển các giá trị thuộc tính và phương thức.


Tags:

Các tin liên quan

Khám phá đối tượng ngày JavaScript
2017-12-06
chuỗi con so với chuỗi con trong JavaScript
2017-11-06
Hiểu Ngày và Giờ trong JavaScript
2017-10-19
Cách xác định các hàm trong JavaScript
2017-10-09
Vòng lặp Đối với, Đối với ... Trong vòng lặp và Đối với ... Trong Vòng lặp trong JavaScript
2017-10-02
Vòng lặp Đối với, Đối với ... Trong vòng lặp và Đối với ... Trong Vòng lặp trong JavaScript
2017-10-02
Xử lý các đối tượng trong JavaScript với Object.assign, Object.keys và hasOwnProperty
2017-09-29
Sử dụng Vòng lặp While và Vòng lặp Do ... Trong khi trong JavaScript
2017-09-27
Sử dụng Vòng lặp While và Vòng lặp Do ... Trong khi trong JavaScript
2017-09-27
Cách sử dụng câu lệnh Switch trong JavaScript
2017-09-11