Nếu đã quen với một ngôn ngữ lập trình hướng đối tượng từ trước, class
là cú pháp rõ ràng nhất để nhận biết cách khai báo một đối tượng mới. JavaScript cũng được coi là một ngôn ngữ lập trình hướng đối tượng, nhưng nếu là một lập trình viên JS đời đầu, cỡ cách đây khoảng chục năm về trước thì JavaScript không hề có cú pháp class
, thay vào đó nó hỗ trợ kế thừa thông qua prototype
. Cách viết này có phần khác biệt với cú pháp của OOP trong đa số ngôn ngữ lập trình hướng đối tượng, nó cũng không được đánh giá cao về khả năng hỗ trợ nhiều tính năng mạnh mẽ của OOP.
Vài năm trở lại đây, ES6 ra đời mang đến từ khóa class
để giúp lập trình viên dễ dàng viết mã theo hướng OOP hơn, hay nói cách khác là thân thiện hơn cho ai đang quen với cú pháp của OOP. Nhưng bản chất đằng sau nó vẫn là sự có mặt của prototype
. Bài viết ngày hôm nay chúng ta sẽ cùng nhau tìm hiểu xem prototype
là gì mà tại sao nó lại quan trọng trong JavaScript nhé.
Prototypes là cơ chế mà các đối tượng JavaScript kế thừa các chức năng từ nhau. Mỗi đối tượng được tạo ra từ một hàm tạo đều có một prototype mặc định, đó là prototype của hàm tạo đó.
Mọi đối tượng trong JavaScript đều có các thuộc tính, phương thức được tích hợp sẵn gọi là prototype. Bản thân prototype cũng là một đối tượng, vì vậy prototype sẽ có prototype riêng, tạo nên một chuỗi prototype. Chuỗi này chỉ kết thúc khi prototype bằng null
.
Prototypes định nghĩa các thuộc tính và phương thức mà mỗi đối tượng được tạo ra từ hàm tạo đó có thể truy cập được. Khi một đối tượng được tạo ra, nó sẽ kế thừa tất cả các thuộc tính và phương thức từ prototype của hàm tạo.
Ví dụ:
const myObject = {
name: "2coffee",
greet() {
console.log(`Greetings from ${this.name}`);
},
};
myObject.greet(); // Greetings from 2coffee
Trong ví dụ trên, greet()
là một phương thức của myObject
, do đó nó có thể gọi phương thức đó. Nhưng ngoài greet()
ra, myObject
là một Object
nên nó kế thừa lại toàn bộ prototype của Object
. Các prototype có trong myObject
lúc này sẽ trông giống như là:
__defineGetter__
__defineSetter__
__lookupGetter__
__lookupSetter__
__proto__
name
constructor
greet
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
toLocaleString
toString
valueOf
myObject
có toàn quyền truy cập vào các prototype này, ví dụ như gọi phương thức toString
.
myObject.toString(); // "[object Object]"
Khi cố gắng truy cập một thuộc tính của một đối tượng, nếu không thể tìm thấy thuộc tính trong chính đối tượng đó, nó sẽ chuyển sang tìm kiếm trong prototype của đối tượng cha. Cứ thế cho đến khi tìm thấy thuộc tính hoặc kết thúc chuỗi kế thừa mà trả về giá trị undefined
.
Ví dụ với một đối tượng Date
trong JavaScript. Date
là một instance của Object
, Date
cũng có prototype riêng nên một đối tượng được tạo mới từ Date
sẽ có ít nhất 3 lớp prototype.
Như tôi đã nói ở trên, prototype sinh ra cho mục đích kế thừa như trong lập trình hướng đối tượng. Việc sử dụng prototype trong JavaScript cho phép ta thêm các thuộc tính và phương thức vào các đối tượng trong một cách hiệu quả hơn. Thay vì tạo các phương thức và thuộc tính cho từng đối tượng, ta có thể thêm chúng vào prototype của hàm tạo, do đó tất cả các đối tượng được tạo ra từ hàm tạo đó đều có thể truy cập đến chúng.
Ví dụ cho một chương trình khai báo lớp Geometry
với hai phương thức calculatePerimeter
(tính chu vi) và calculateArea
(tính diện tích). Hai lớp Square
và Circle
kế thừa từ Geometry
thông qua prototype được viết như sau:
// Khai báo lớp Geometry
function Geometry() {}
// Phương thức tính chu vi
Geometry.prototype.calculatePerimeter = function() { return; }
// Phương thức tính diện tích
Geometry.prototype.calculateArea = function() { return; }
// Khai báo lớp Square kế thừa từ Geometry
function Square(sideLength) {
this.sideLength = sideLength;
}
Square.prototype = Object.create(Geometry.prototype);
Square.prototype.constructor = Square;
// Override phương thức tính chu vi cho Square
Square.prototype.calculatePerimeter = function() {
return this.sideLength * 4;
}
// Override phương thức tính diện tích cho Square
Square.prototype.calculateArea = function() {
return this.sideLength * this.sideLength;
}
// Khai báo lớp Circle kế thừa từ Geometry
function Circle(radius) {
this.radius = radius;
}
Circle.prototype = Object.create(Geometry.prototype);
Circle.prototype.constructor = Circle;
// Override phương thức tính chu vi cho Circle
Circle.prototype.calculatePerimeter = function() {
return 2 * Math.PI * this.radius;
}
// Override phương thức tính diện tích cho Circle
Circle.prototype.calculateArea = function() {
return Math.PI * this.radius * this.radius;
}
Kể từ ES6, JavaScript mang đến cho chúng ta cú pháp mới để kế thừa. Đó chính là từ khóa class
, bằng cách sử dụng mới này, giúp chúng ta quen thuộc hơn với cú pháp giống như lập trình hướng đối tượng từ các ngôn ngữ khác.
Ví dụ cũng là chương trình trên nhưng viết lại bằng class mã sẽ trông gọn gàng hơn rất nhiều:
// Khai báo lớp Geometry
class Geometry {
calculatePerimeter() { return; }
calculateArea() { return; }
}
// Khai báo lớp Square kế thừa từ Geometry
class Square extends Geometry {
constructor(sideLength) {
super();
this.sideLength = sideLength;
}
calculatePerimeter() {
return this.sideLength * 4;
}
calculateArea() {
return this.sideLength * this.sideLength;
}
}
// Khai báo lớp Circle kế thừa từ Geometry
class Circle extends Geometry {
constructor(radius) {
super();
this.radius = radius;
}
calculatePerimeter() {
return 2 * Math.PI * this.radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
class
có từ ES6, tuy nhiên nó vẫn có nhiều hạn chế so với OOP trong các ngôn ngữ thuần hướng đối tượng khác như Java, C#... Một phần là do bản chất của class
vẫn là dựa trên prototype, có điều nó tạo ra để giảm sự phức tạp của mã, giúp mã dễ đọc và gọn gàng hơn.
Prototypes là một khái niệm rất quan trọng trong JavaScript vì nó cho phép ta tạo ra các đối tượng với tính kế thừa và sử dụng lại mã nguồn một cách hiệu quả.
Các đối tượng được tạo ra từ cùng một hàm tạo đều có thể chia sẻ các thuộc tính và phương thức được định nghĩa trong prototype của hàm tạo đó. Điều này giúp giảm sự trùng lặp trong mã nguồn và tiết kiệm bộ nhớ cho ứng dụng.
Nếu để ý, bạn sẽ thấy gần như mọi thứ trong JavaScript đều xuất phát từ prototype. Tạo ra một object mới, nó sẽ kế thừa toàn bộ prototype từ Object, tạo ra một function, nó sẽ kế thừa toàn bộ prototype từ Function...
Ngoài ra, việc sử dụng prototype cũng cho phép ta tạo các đối tượng con kế thừa các thuộc tính và phương thức từ đối tượng cha. Điều này giúp tăng tính tái sử dụng trong mã nguồn, giảm thiểu sự trùng lặp và tăng tính bảo trì của ứng dụng.
Thêm vào đó, prototype cũng cung cấp một cách để mở rộng các đối tượng có sẵn trong JavaScript. Bằng cách thêm các thuộc tính và phương thức vào prototype của đối tượng, ta có thể mở rộng tính năng của đối tượng đó mà không cần thay đổi mã nguồn gốc.
Do đó, prototype là một khái niệm quan trọng trong JavaScript và là một trong những cách để tăng tính linh hoạt, hiệu quả và bảo trì sau này.
Prototypes là cơ chế mà các đối tượng JavaScript kế thừa các chức năng từ nhau. Lập trình viên đời đầu phải viết mã kế thừa dựa trên prototype. Vài năm trở lại đây, ES6 ra đời mang đến cú pháp class
giúp việc viết mã trở nên thân thiện hơn khi giờ đây các cú pháp của nó gần với ngôn ngữ hướng đối tượng hơn. Prototypes là một khái niệm quan trọng trong JavaScript, hầu như mọi thứ đều dựa trên prototype để hoạt động như Object, Function... vì thế hiểu rõ bản chất của Prototypes sẽ giúp cho việc học hỏi thêm kiến thức sau này.
Tài liệu tham khảo:
Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.
Bình luận (0)