Giới Thiệu
OOP (Lập trình hướng đối tượng) có lẽ là một trong những khái niệm đầu tiên mà chúng ta học được khi bắt đầu lập trình. Từ trường học, các hướng dẫn cho đến những lần ôn tập trước khi thăng chức, chúng ta đều được học về các lớp (class) như là những bản thiết kế và các đối tượng (object) như là những thể hiện của chúng. Cảm giác như OOP có mặt khắp nơi: Java, C++, Python… OOP ở khắp nơi.
Vì vậy, khi bắt đầu viết mã JavaScript, chúng ta mong đợi các quy tắc tương tự sẽ được áp dụng. Các lớp, đối tượng, kế thừa — tất cả đều có mặt. Nhưng đây là điều thú vị: JavaScript giả lập OOP rất tốt — nhưng thực chất, nó lại rất khác biệt.
Hãy cùng khám phá sự thật về OOP trong JavaScript, sử dụng ví dụ rất quen thuộc về một Con Người.
Vấn Đề Ví Dụ
Khi OOP được dạy, thường thì chúng ta sẽ chọn một phép ẩn dụ từ thực tế để dễ hiểu hơn. Vậy hãy làm điều tương tự ở đây.
Chúng ta muốn đại diện cho một Con Người. Một Con Người có name, age, có thể greet() người khác và cũng sẽ già đi với happyBirthday().
Nhưng có một điều cần lưu ý: hãy quên OOP đi trong một khoảnh khắc. Làm thế nào chúng ta có thể xây dựng điều này theo cách của JavaScript?
Tạo Ra Một Con Người
Hãy giữ cho nó thật đơn giản, và chỉ cần tạo ra một Con Người.
javascript
let human1 = {
name: "Adam",
age: 25,
greet: function () {
console.log(`Chào, tôi là ${this.name}, ${this.age} tuổi.`);
},
happyBirthday: function () {
this.age++;
console.log(`Hôm nay là sinh nhật của tôi! Tôi giờ ${this.age} tuổi.`);
}
};
human1.greet(); // Chào, tôi là Adam, 25 tuổi.
human1.happyBirthday(); // Hôm nay là sinh nhật của tôi! Tôi giờ 26 tuổi.
Chúng ta đã tạo ra một Con Người, nhưng điều này chỉ cho chúng ta một Con Người. Nếu muốn nhiều hơn, chúng ta sẽ phải sao chép toàn bộ đối tượng này. Quá nhiều công sức, vậy hãy giảm bớt và tạo ra một Nhà Máy Con Người.
Mẫu Nhà Máy (Factory Pattern)
javascript
function createHuman(name, age) {
return {
name,
age,
greet: function () {
console.log(`Chào, tôi là ${this.name}, ${this.age} tuổi.`);
},
happyBirthday: function () {
this.age++;
console.log(`Hôm nay là sinh nhật của tôi! Tôi giờ ${this.age} tuổi.`);
}
};
}
const human1 = createHuman("Bob", 30);
human1.greet(); // Chào, tôi là Bob, 30 tuổi.
human1.happyBirthday(); // Hôm nay là sinh nhật của tôi! Tôi giờ 31 tuổi.
Giờ tôi có thể tạo ra nhiều Con Người như tôi muốn. Thật tuyệt! Nhưng khoan đã… có vấn đề! Mỗi Con Người sẽ có bản sao riêng của greet và happyBirthday. Nếu tôi tạo 1,000 Con Người, sẽ có 1,000 hàm trùng lặp trong bộ nhớ. Thật kinh khủng.
Chắc chắn JavaScript có một bí mật cho điều này.
Bí Mật Được Tiết Lộ
Thực ra JS có một bí mật, thay vì sao chép phương thức cho mỗi đối tượng, JavaScript một cách lén lút cung cấp cho mỗi đối tượng một liên kết ngầm. Thông qua liên kết này, các đối tượng có thể mượn phương thức từ một nơi chung — không cần sao chép.
javascript
const humanMethods = {
greet: function () {
console.log(`Chào, tôi là ${this.name}, ${this.age} tuổi.`);
},
happyBirthday: function () {
this.age++;
console.log(`Hôm nay là sinh nhật của tôi! Tôi giờ ${this.age} tuổi.`);
}
};
function createHuman(name, age) {
let human = Object.create(humanMethods); // <-- liên kết ngầm
human.name = name;
human.age = age;
return human;
}
const h1 = createHuman("Charlie", 22);
h1.greet(); // Chào, tôi là Charlie, 22 tuổi.
h1.happyBirthday(); // Hôm nay là sinh nhật của tôi! Tôi giờ 23 tuổi.
Liên kết ngầm này thực chất là một thuộc tính ẩn gọi là __proto__, mà mỗi đối tượng JavaScript đều có.
👉 Và nhớ rằng, trong JavaScript mọi thứ đều là đối tượng (thực tế… ngoại trừ một vài kiểu nguyên thủy, nhưng ngay cả chúng cũng hành xử như đối tượng khi cần thiết).
Khi JS chạy và không thể tìm thấy một thuộc tính trực tiếp trên đối tượng của bạn, nó không hoảng sợ. Thay vào đó, nó sẽ theo dõi dấu vết __proto__ này, tìm kiếm thuộc tính trong đối tượng liên kết. Đó là cách mà greet và happyBirthday hoạt động kỳ diệu mà không bị sao chép.
Thật tuyệt đúng không?
Tiếp Cận new
Nhưng việc kết nối những liên kết ẩn này bằng Object.create vẫn cảm thấy hơi thủ công. Liệu không có cách nào sạch sẽ hơn mà JavaScript có thể cung cấp không?
Thực tế, các hàm tự thân mang một sức mạnh bí mật: chúng cũng là đối tượng, và mỗi hàm có một thuộc tính “hậu trường” riêng gọi là prototype. Khi bạn kết hợp điều đó với từ khóa new… boom, bạn có một cách sạch sẽ hơn để kết nối mọi thứ.
javascript
function Human(name, age) {
this.name = name;
this.age = age;
}
Human.prototype.greet = function () {
console.log(`Chào, tôi là ${this.name}, ${this.age} tuổi.`);
};
Human.prototype.happyBirthday = function () {
this.age++;
console.log(`Hôm nay là sinh nhật của tôi! Tôi giờ ${this.age} tuổi.`);
};
const h1 = new Human("Dave", 40);
h1.greet(); // Chào, tôi là Dave, 40 tuổi.
h1.happyBirthday(); // Hôm nay là sinh nhật của tôi! Tôi giờ 41 tuổi.
Như vậy thật sạch sẽ! Giờ liên kết “ẩn” được quản lý tự động bởi new.
👉 Bạn có thắc mắc về cách mà liên kết ẩn này (hay còn gọi là prototypes) thực sự hoạt động dưới nắp không?
💬 Hãy để lại bình luận bên dưới nếu bạn muốn tôi viết một bài phân tích đầy đủ về prototypes trong blog tiếp theo của mình.
Có cảm giác như một lớp (class), đúng không? Nhưng đừng bị lừa. Human chỉ là một hàm, và new Human() tạo ra một đối tượng JS thuần liên kết với Human.prototype. Vẫn chưa có lớp thực sự nào ở đây.
Màn Kết Cuối - Các Lớp
Cho đến nay, chúng ta đã đi từ việc sao chép các đối tượng → các hàm nhà máy → các hàm khởi tạo với prototypes. Mỗi bước đã đưa chúng ta đến gần hơn với điều gì đó trông giống như OOP, trong khi vẫn được thúc đẩy bởi phép thuật prototype ẩn của JavaScript.
Và rồi ES6 xuất hiện và nói:
“Hey các lập trình viên, tôi thấy bạn đang giả lập các lớp, tại sao không làm cho nó trông quen thuộc hơn?”
Giới thiệu class.
javascript
class Human {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Chào, tôi là ${this.name}, ${this.age} tuổi.`);
}
happyBirthday() {
this.age++;
console.log(`Hôm nay là sinh nhật của tôi! Tôi giờ ${this.age} tuổi.`);
}
}
const h1 = new Human("Eve", 28);
h1.greet(); // Chào, tôi là Eve, 28 tuổi.
h1.happyBirthday(); // Hôm nay là sinh nhật của tôi! Tôi giờ 29 tuổi.
Giờ thì nó thật sự giống như OOP. Cảm giác quen thuộc như với JAVA, C++, Python…
Nhưng đây là điều thú vị:
👉 class trong JavaScript không phát minh ra một hệ thống OOP mới. Nó chỉ là đường cú pháp đơn thuần.
Phía sau bức màn, class Human vẫn chỉ là một hàm khởi tạo. Các phương thức của nó? Vẫn được gắn với Human.prototype. Liên kết “ẩn” mà chúng ta đã nói đến? Vẫn đảm nhiệm công việc nặng nhọc.
Điểm khác biệt duy nhất là cách mà nó trông đẹp hơn khi bạn gõ.
Vì vậy, đúng là JS cung cấp cho chúng ta bộ trang phục OOP đầy đủ — nhưng đừng bị lừa. Nền tảng vẫn là prototypes, mọi thứ đều như vậy.
javascript
console.log(typeof Human); // function
Tại Sao Điều Này Quan Trọng
Ngày nay, với GenAI và các công cụ tự động viết mã cho chúng ta, thật dễ dàng để bỏ qua các nguyên tắc cơ bản. Nhưng khi mọi thứ gặp sự cố — hoặc khi có điều gì mới xuất hiện — hiểu những gì thực sự xảy ra dưới nắp là sự khác biệt giữa việc vật lộn và phát triển như một lập trình viên.
OOP của JavaScript là một ví dụ hoàn hảo. Nó trông quen thuộc, nhưng thực chất lại rất khác biệt. Khi bạn nhìn thấy sự ảo tưởng này, bạn sẽ có được sự hiểu biết sâu sắc hơn về ngôn ngữ, và đó chính là điều thực sự làm cho bạn trở thành một kỹ sư giỏi hơn.