Hiểu Sâu Class Và Prototype JavaScript: Tối Ưu Hiệu Suất Và Giảm Thiểu Lỗi
JavaScript là một trong những ngôn ngữ lập trình phổ biến nhất trong lập trình front-end trên trình duyệt. Việc hiểu rõ cách hoạt động của Class và Prototype không chỉ giúp bạn viết mã sáng sủa, dễ bảo trì mà còn tối ưu hiệu suất, giảm thiểu lỗi trong các dự án phức tạp. Hãy cùng đi sâu vào cách JavaScript quản lý Prototype và lý do vì sao điều này lại quan trọng đến vậy.
#1. Khái quát về Class và Prototype
#1.1. Tại sao cần hiểu Class và Prototype?
Trong JavaScript, khái niệm Class (từ phiên bản ES6) được giới thiệu để giúp lập trình viên mô phỏng tư tưởng lập trình hướng đối tượng (OOP) một cách trực quan hơn. Tuy nhiên, JavaScript thực chất vẫn hoạt động dựa trên Prototype. Việc hiểu rõ Prototype giúp bạn:
- Xác định cách các thuộc tính và phương thức được chia sẻ giữa nhiều đối tượng.
- Tiết kiệm bộ nhớ và tối ưu hiệu suất khi tạo nhiều đối tượng cùng loại.
- Giải quyết những lỗi thường gặp liên quan đến ngữ cảnh (
this) và quá trình kế thừa (inheritance).
#1.2. Prototype được dùng chung như thế nào?
Khi bạn tạo một đối tượng mới từ một hàm tạo (constructor) hoặc từ một class (ES6), JavaScript sẽ gắn đối tượng này với prototype của nó. Tất cả các phương thức chung được đặt trong đối tượng prototype. Điều này có nghĩa là bất kỳ thuộc tính hoặc phương thức nào nằm trong prototype sẽ được tất cả đối tượng con (instances) cùng chia sẻ.
Ví dụ ngắn với hàm tạo ES5:
1 | function Person(name) { |
Trong ví dụ này, cả person1 và person2 đều chia sẻ chung phương thức greet trong Person.prototype. Nhờ đó, bạn không phải khai báo phương thức greet lặp lại trong từng đối tượng.
#1.3. Biến this được hiểu như thế nào?
Trong JavaScript, this trỏ tới đối tượng gọi phương thức tại thời điểm phương thức được gọi. Nguyên tắc xác định ngữ cảnh của this có thể hiểu nôm na là “đối tượng nằm ở bên trái dấu chấm (.) khi gọi hàm sẽ là this”.
- Ví dụ:
instance1.doSomething()=>thistrongdoSomethingchính làinstance1.
Phần khó của JavaScript nằm ở chỗ this không cố định, nó phụ thuộc cách bạn gọi hàm. Ví dụ, nếu tách rời hàm doSomething ra một biến, rồi gọi hàm đó độc lập, this lúc đó có thể trở thành undefined (trong strict mode) hoặc window (trong non-strict mode).
#2. So sánh Class (ES6) với Function Constructor (ES5)
#2.1. Ví dụ với ES5 (Function Constructor)
Trước ES6, lập trình viên thường khai báo hàm tạo (function constructor) và gán phương thức vào prototype:
1 | function Animal(name) { |
- Ở đây, tất cả phương thức dùng chung (trong ví dụ là
speak) được đặt vàoAnimal.prototype. - Khi gọi
dog.speak(), JavaScript sẽ tìm phương thứcspeaktrong đối tượngdog. Không thấy, nó sẽ tra cứu (lookup) trongAnimal.prototype. thissẽ được ràng buộc về đối tượngdogtại thời điểm gọidog.speak().
#2.2. Ví dụ với ES6 (Class)
Từ ES6, JavaScript bổ sung cú pháp class để mô phỏng OOP rõ ràng hơn. Tuy nhiên, về bản chất, nó vẫn hoạt động dựa trên prototype:
1 | class Animal { |
- Cú pháp
classchỉ là “đường tắt” để định nghĩa function constructor và các phương thức prototype. - Phía sau, JavaScript vẫn tạo một hàm constructor tên
Animalvà gán các phương thứcspeakvàoAnimal.prototypetương tự như ở ES5.
#Quan sát Prototype
Bạn có thể kiểm tra prototype của một đối tượng bằng thuộc tính __proto__ (chỉ nên dùng để debug) hoặc dùng phương thức Object.getPrototypeOf(cat):
1 | console.log(Object.getPrototypeOf(cat) === Animal.prototype); |
#Ứng dụng thực tế
Trong dự án thực tế trên trình duyệt, sử dụng class ES6 giúp mã dễ đọc hơn và giảm thiểu nhầm lẫn so với function constructor ES5. Tuy nhiên, việc nắm chắc prototype và this vẫn rất quan trọng khi cần tối ưu hoặc gỡ lỗi.
#2.3. Mở rộng class (extends) trong ES6 so với ES5
Một trong những tính năng quan trọng nhất của OOP là kế thừa (inheritance). Trong JavaScript, chúng ta cũng có thể kế thừa từ một “lớp cha” (hay hàm tạo cha) để tạo ra “lớp con” (hay hàm tạo con) và chia sẻ các phương thức chung.
#Kế thừa trong ES5 (Function Constructor)
Trước ES6, bạn có thể “kế thừa” thủ công bằng cách:
- Gọi constructor của lớp cha trong constructor của lớp con (sử dụng
callhoặcapply). - Tạo mới prototype của lớp con dựa trên prototype của lớp cha (
Object.create). - Đặt lại thuộc tính
constructorcho prototype của lớp con.
Ví dụ:
1 | function Animal(name) { |
- Ở đây,
Dog“thừa kế” các phương thức từAnimal.prototype. - Nhờ đó, chúng ta có thể gọi
dog1.speak()và cảdog1.bark().
#Kế thừa trong ES6 (Class)
Với cú pháp class, bạn có thể kế thừa dễ dàng hơn bằng từ khóa extends. Để gọi constructor của lớp cha, bạn dùng từ khóa super() trong constructor của lớp con:
1 | class Animal { |
extendscho phépDogkế thừa các phương thức từAnimalmột cách rõ ràng, không cần thao tác thủ công như ES5.- Bên trong constructor của lớp con, bạn bắt buộc gọi
super(...)trước khi truy cậpthis, nếu không sẽ bị lỗi. - Phía dưới “bộ máy” JavaScript,
Dog.prototypevẫn kế thừa từAnimal.prototype, tương tự cách chúng ta làm thủ công trong ES5.
#Tính thực tiễn
- Dùng cú pháp
classvàextendsgiúp mã ngắn gọn, dễ đọc, giảm thiểu sai sót. - Khi làm việc với các dự án lớn, kế thừa là mô hình quan trọng để tổ chức code, chia sẻ logic giữa các đối tượng.
- Việc hiểu rõ nguyên lý kế thừa thông qua prototype giúp bạn debug và tối ưu hơn, đặc biệt nếu cần thao tác nâng cao hoặc bắt gặp những đoạn code ES5 cũ.
#3. Lỗi thường gặp và cách tối ưu
#3.1. Gọi sai ngữ cảnh this
Một lỗi phổ biến là khi bạn tách phương thức khỏi đối tượng, sau đó gọi nó ở ngữ cảnh khác:
1 | const someMethod = dog2.speak; |
#Cách khắc phục:
- Dùng
bindhoặc arrow function (nếu phù hợp) để cố định ngữ cảnhthis. - Hoặc luôn gọi qua đối tượng:
dog2.speak().
#3.2. Phòng tránh vòng lặp vô hạn
Ví dụ, nếu trong constructor hay phương thức prototype, bạn vô tình gọi chính phương thức đang thực thi nhưng không có điều kiện dừng, có thể dẫn đến vòng lặp vô hạn và treo trình duyệt.
1 | function Circle(radius) { |
#Cách khắc phục:
- Kiểm tra logic kỹ càng, tránh tự gọi lại chính hàm mà không có điều kiện dừng rõ ràng.
- Sử dụng kỹ thuật debug (như
console.log) hoặc breakpoints trên trình duyệt để theo dõi luồng chương trình.
#4. Bài tập và câu hỏi tự kiểm tra
#4.1. Bài tập
Refactor function constructor từ ES5 sang ES6 Class
- Bạn có sẵn một function constructor tên
Product(ES5) có thuộc tínhtitle,pricevà một phương thứcgetSummary(). - Hãy viết lại bằng cú pháp Class (ES6) với logic tương tự.
- Kiểm tra xem kết quả có giống nhau không khi gọi
getSummary()trên cùng dữ liệu.
- Bạn có sẵn một function constructor tên
Quản lý giỏ hàng (Cart) với Prototype trong ES5
- Tạo function constructor
Cartvới thuộc tínhitemslà một mảng rỗng. - Thêm phương thức
addItem(productName)vàoCart.prototypeđể đẩy một chuỗiproductNamevàoitems. - Thêm phương thức
getItems()trả về mảngitems. - Tạo hai giỏ hàng
cartAvàcartB. Thử thêm các sản phẩm khác nhau vào mỗi giỏ hàng và in ra kết quả.
- Tạo function constructor
Kiểm tra ngữ cảnh
thiskhi tách phương thức- Tạo một đối tượng
menuvới một phương thứcshowMenu()in ra"Display main menu". - Gán phương thức này vào biến
displayMenu = menu.showMenu. - Gọi
displayMenu()và quan sát kết quả, có lỗi hay không? Tại sao? - Áp dụng
bindhoặc arrow function để sửa lỗi (nếu có).
- Tạo một đối tượng
#4.2. Câu hỏi ôn tập
Vì sao cú pháp ES6 Class chỉ là “đường tắt” của prototype?
- Chỉ ra các điểm tương đồng khi so sánh code ES5 (function constructor + prototype) và code ES6 (class).
Bạn hiểu như thế nào về nguyên tắc “prototype chain”?
- Cho ví dụ về việc tra cứu phương thức qua nhiều cấp prototype.
Trình bày cách khắc phục lỗi “Cannot read property ‘xxx’ of undefined” khi gọi hàm mà không có đối tượng đứng trước dấu chấm.
- Nêu ít nhất hai cách giải quyết.
Tại sao
__proto__bị xem là không khuyến khích, và nên dùng phương thức nào thay thế?- Lợi ích của việc dùng
Object.getPrototypeOf()vàObject.setPrototypeOf()là gì?
- Lợi ích của việc dùng
Nếu bạn muốn chuyển một class sang một module (ES Module) để dùng trong nhiều trang khác nhau, bạn cần làm gì?
- Gợi ý: sử dụng
exportvàimport.
- Gợi ý: sử dụng
#5. Kết luận
Hiểu rõ Class và Prototype trong JavaScript không chỉ giúp bạn viết mã sạch và dễ bảo trì, mà còn mang lại lợi ích lâu dài về mặt hiệu suất và khả năng mở rộng. Khi các dự án trở nên lớn và phức tạp, kiến thức này sẽ giúp bạn tối ưu hóa mã và giảm thiểu lỗi hiệu quả hơn. Hãy áp dụng những kiến thức này vào thực tế qua các ví dụ, bài tập để làm chủ JavaScript cả về lý thuyết lẫn thực hành.