This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
JavaScript là một ngôn ngữ lập trình đa mô hình. Khác với C++ hay Java sử dụng mô hình hướng đối tượng
dựa trên Class truyền thống (Class-based OOP), JavaScript nguyên bản sử dụng
Mô hình kế thừa dựa trên nguyên mẫu (Prototypal OOP). Dù ES6 mang lại cú pháp
class trông rất giống các ngôn ngữ biên dịch khác, bản chất bên dưới vẫn chạy trên nền
tảng chuỗi nguyên mẫu Prototype.
1. Chuỗi Prototype, Kế Thừa Nguyên Mẫu & Cấu Trúc Bộ Nhớ
Mỗi đối tượng (Object) trong JavaScript đều có một liên kết ẩn trỏ đến một đối tượng khác gọi là
Prototype (nguyên mẫu) của nó. Liên kết nội bộ này được thể hiện thông qua thuộc tính
ẩn [[Prototype]] (hoặc có thể truy cập bằng getter/setter __proto__ trên môi
trường runtime của trình duyệt).
Khi bạn truy cập một phương thức hay thuộc tính trên một đối tượng, JS Engine sẽ thực hiện quy trình tìm kiếm dọc theo Prototype Chain (Chuỗi nguyên mẫu):
- Kiểm tra xem thuộc tính đó có nằm trực tiếp trên bản thân đối tượng đó hay không (Own properties).
-
Nếu không có, nó sẽ đi dọc theo liên kết
[[Prototype]]lên đối tượng cha nguyên mẫu để tìm kiếm. -
Quá trình này lặp lại cho đến khi gặp prototype gốc là
null(thường là kết thúc củaObject.prototype). Nếu vẫn không tìm thấy, kết quả trả về sẽ làundefined.
Sơ đồ bộ nhớ của một chuỗi Prototype hoàn chỉnh:
┌───────────────────────────────┐
│ myDog (Instance) │
├───────────────────────────────┤
│ name = "Lu" │
│ breed = "Golden" │
│ __proto__ ───────────────────┼──┐
└───────────────────────────────┘ │
▼
┌───────────────────────────────────────────────┐
│ Dog.prototype │
├───────────────────────────────────────────────┤
│ bark() { ... } │
│ __proto__ ───────────────────────────────────┼──┐
└───────────────────────────────────────────────┘ │
▼
┌───────────────────────────────────────────────────────────────┐
│ Animal.prototype │
├───────────────────────────────────────────────────────────────┤
│ speak() { ... } │
│ __proto__ ───────────────────────────────────────────────────┼──┐
└───────────────────────────────────────────────────────────────┘ │
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Object.prototype (Điểm dừng cuối cùng) │
├─────────────────────────────────────────────────────────────────────────────┤
│ toString(), hasOwnProperty() │
│ __proto__ ─────────────────────────────────────────────────────────────────┼──► null
└─────────────────────────────────────────────────────────────────────────────┘
Mẹo tối ưu hóa bộ nhớ: Tạo đối tượng sạch với
Object.create(null)
Khi bạn tạo một object rỗng thông thường bằng {}, object đó mặc định kế thừa toàn bộ các
phương thức từ Object.prototype. Điều này không chỉ tiêu tốn thêm tài nguyên tra cứu mà
còn dẫn đến lỗ hổng bảo mật nghiêm trọng mang tên
Prototype Pollution (Ô nhiễm nguyên mẫu) nếu ứng dụng nhận dữ liệu không an toàn từ
bên ngoài.
Hãy sử dụng Object.create(null) để tạo ra các dictionary "sạch hoàn toàn" không kế thừa
bất kỳ prototype nào (kể cả toString hay hasOwnProperty đều không tồn tại),
tăng tốc độ tra cứu khóa tối đa.
2. ES6 Class Dưới Nắp Máy (Under The Hood)
ES6 mang đến từ khóa class để viết code hướng đối tượng quen thuộc hơn. Tuy nhiên, đây
thực chất chỉ là "Syntactic Sugar" (Đường cú pháp). Dưới nắp máy, trình duyệt (như
V8) vẫn tự động biên dịch cú pháp Class thành Constructor Function và chuỗi Prototype truyền thống:
class User {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(this.name);
}
}
function User(name) {
this.name = name;
}
User.prototype.sayHi = function() {
console.log(this.name);
};
Cơ chế hoạt động của Private Fields (#properties):
Khác với các thuộc tính thông thường, các thuộc tính private bắt đầu bằng dấu # được V8
hiện thực hóa bằng cơ chế Private Symbols ẩn trong engine. Chúng hoàn toàn cô lập,
không thể bị duyệt thấy bằng các phương thức phản chiếu thông thường như
Object.keys() hay Object.getOwnPropertyNames(), đảm bảo tính đóng gói tuyệt
đối ở cấp độ engine.
3. Làm Chủ Ngữ Cảnh Hoạt Đủa Từ Khóa 'this'
Khác với các ngôn ngữ biên dịch tĩnh nơi
this được xác định tại thời điểm viết code, trong JavaScript this được ràng
buộc động tại thời điểm gọi hàm (Runtime Binding), tuân theo 4 quy tắc ưu tiên từ
thấp đến cao:
-
Default Binding (Ràng buộc mặc định): Khi gọi một hàm độc lập thông thường (ví dụ:
show()). Trong chế độ strict mode,thissẽ làundefined. Ngược lại, nó trỏ về đối tượng toàn cụcwindow(trình duyệt) hoặcglobal(Node.js). -
Implicit Binding (Ràng buộc ngầm định): Khi gọi một phương thức thuộc về một đối
tượng (ví dụ:
obj.method()).thischính là đối tượng đứng trước dấu chấm (obj). -
Explicit Binding (Ràng buộc tường minh):
Khi bạn ép buộc `this` trỏ về một đối tượng mong muốn bằng cách dùng
call(),apply(), hoặcbind(). Phương thứcbind()trả về một đối tượng hàm đặc biệt (Bound Function Exotic Object) mà `this` bên trong nó đã bị khóa chặt và không thể ghi đè bởi bất kỳ lời gọi nào khác sau đó. -
New Binding (Khởi tạo đối tượng): Khi gọi hàm constructor với từ khóa
new.
4 Bước hoạt động của toán tử new dưới góc nhìn V8:
Khi bạn thực hiện
const obj = new MyConstructor(), JS Engine sẽ thực thi chính xác 4 bước sau:
// 1. Tạo một object rỗng mới tinh
const newObj = {};
// 2. Thiết lập liên kết __proto__ của newObj trỏ vào prototype của Constructor
Object.setPrototypeOf(newObj, Constructor.prototype);
// 3. Thực thi hàm Constructor với context 'this' là newObj vừa tạo
const result = Constructor.apply(newObj, args);
// 4. Trả về newObj, trừ khi hàm Constructor chủ động trả về một object khác
return (typeof result === "object" && result !== null) ? result : newObj;
Lexical This (Arrow Function): Hàm mũi tên () => {} không có thuộc
tính this riêng biệt. Nó thừa hưởng this trực tiếp từ phạm vi chứa nó bên
ngoài ngay lúc định nghĩa (Lexical context). Mọi nỗ lực ghi đè this của arrow function
bằng call, apply, bind đều vô hiệu.
4. Code Thực Hành Nâng Cao: class_oop.js
Đoạn mã mẫu hoàn chỉnh dưới đây minh họa chi tiết Prototypal Inheritance, Class ES6 với thuộc tính
private, cách tự viết hàm giả lập toán tử new và cấu trúc tạo Dictionary sạch:
/**
* Bài 2: Prototype, ES6 Class, OOP, Từ khóa 'this', và cơ chế hoạt động của toán tử 'new'
* Giáo trình tự học JavaScript - js-tools.org
*/
console.log("=== 1. Prototypal Inheritance (Kế thừa nguyên mẫu) ===");
// Khởi tạo đối tượng cơ sở bằng constructor function kiểu cũ
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} đang phát ra âm thanh.`);
};
// Khởi tạo đối tượng Dog kế thừa từ Animal
function Dog(name, breed) {
Animal.call(this, name); // B1: Gọi constructor cha và liên kết 'this'
this.breed = breed;
}
// B2: Thiết lập chuỗi prototype kế thừa phương thức
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} đang sủa gâu gâu!`);
};
const myDog = new Dog("Lu", "Golden");
myDog.speak(); // Kế thừa phương thức từ Animal.prototype
myDog.bark(); // Phương thức riêng trên Dog.prototype
console.log("\n=== 2. ES6 Class & Encapsulation (Class ES6 dưới nắp máy) ===");
class User {
// Thuộc tính private (Được V8 lưu trữ dưới dạng Private Symbols nội bộ)
#password;
constructor(username, password) {
this.username = username;
this.#password = password;
}
get passwordHint() {
return "********";
}
verifyPassword(inputPassword) {
return this.#password === inputPassword;
}
}
class Admin extends User {
constructor(username, password, role) {
super(username, password); // Gọi constructor cha
this.role = role;
}
showRole() {
console.log(`Admin ${this.username} có quyền: ${this.role}`);
}
}
const adminObj = new Admin("quang_admin", "SuperSecr3t", "Owner");
adminObj.showRole();
console.log("Mật khẩu hint qua getter:", adminObj.passwordHint);
try {
eval("adminObj.#password");
} catch (e) {
console.log("Không thể truy cập thuộc tính private:", e.message);
}
console.log("\n=== 3. Từ khóa 'this' và cơ chế liên kết (Binding) ===");
const person = {
name: "Hoàng",
greet() {
console.log(`Xin chào, tôi tên là ${this.name}`);
}
};
person.greet(); // Implicit Binding -> 'this' trỏ về đối tượng trước dấu chấm (person)
const unboundGreet = person.greet;
console.log("Gọi hàm không liên kết (this bị mất):");
try {
unboundGreet(); // undefined hoặc lỗi trong strict mode
} catch (e) {
console.log("Lỗi:", e.message);
}
// Explicit Binding với bind()
console.log("Gọi hàm sau khi liên kết tường minh:");
const boundGreet = unboundGreet.bind(person);
boundGreet();
const guest = { name: "Nam" };
unboundGreet.call(guest); // call() liên kết 'this' tạm thời thành guest
// 4. Giả lập cơ chế hoạt động của toán tử 'new'
console.log("\n=== 4. Custom 'new' Operator Simulation ===");
function myNew(Constructor, ...args) {
// Bước 1: Tạo một đối tượng rỗng mới
const newObj = {};
// Bước 2: Thiết lập liên kết __proto__ trỏ vào prototype của Constructor
Object.setPrototypeOf(newObj, Constructor.prototype);
// Bước 3: Thực thi Constructor với từ khóa 'this' là newObj vừa tạo
const result = Constructor.apply(newObj, args);
// Bước 4: Trả về object mới, trừ khi Constructor chủ động trả về một object khác
return (typeof result === "object" && result !== null) ? result : newObj;
}
// Test hàm myNew
const customDog = myNew(Dog, "Ki", "Poodle");
customDog.speak();
customDog.bark();
// 5. Dictionary sạch không có Prototype (Object.create(null))
console.log("\n=== 5. Clean Dictionary using Object.create(null) ===");
const dirtyDict = {}; // Kế thừa từ Object.prototype
const cleanDict = Object.create(null); // Hoàn toàn trống rỗng
console.log("dirtyDict.toString:", typeof dirtyDict.toString); // 'function'
console.log("cleanDict.toString:", typeof cleanDict.toString); // 'undefined' (tránh lỗi prototype pollution)
Trắc nghiệm 1: Arrow Function & this
Từ khóa this của một Arrow Function được quyết định dựa trên quy tắc
nào?
5. Playground: Prototype Chain Explorer
Hãy tự chạy và sửa đổi đoạn code dưới đây để cảm nhận cách Prototype Chain hoạt động — quan sát từng
bước lookup từ instance lên đến Object.prototype:
6. Getter/Setter & Static Members trong Class
Getter cho phép tính toán thuộc tính theo yêu cầu (lazy computation) thay vì lưu sẵn
trong constructor, tiết kiệm bộ nhớ và đảm bảo giá trị luôn mới nhất. Setter cho phép
kiểm tra giá trị trước khi gán. Cả hai được định nghĩa bằng từ khóa get và
set bên trong class:
class Circle {
#radius; // private field
constructor(radius) {
this.#radius = radius;
}
// Getter: tính theo yêu cầu, không lưu trong memory
get area() {
return Math.PI * this.#radius ** 2;
}
get perimeter() {
return 2 * Math.PI * this.#radius;
}
// Setter: validate trước khi gán
set radius(value) {
if (value <= 0) throw new RangeError("Bán kính phải > 0");
this.#radius = value;
}
get radius() {
return this.#radius;
}
// Static: thuộc về class, không phải instance
static compare(c1, c2) {
return c1.area - c2.area;
}
// Static factory method: thay thế cho constructor phức tạp
static fromDiameter(d) {
return new Circle(d / 2);
}
}
const c1 = new Circle(5);
console.log("Area:", c1.area.toFixed(2)); // 78.54
console.log("Perimeter:", c1.perimeter.toFixed(2)); // 31.42
c1.radius = 10; // qua setter
console.log("New area:", c1.area.toFixed(2)); // 314.16
const c2 = Circle.fromDiameter(6); // factory method
console.log("Compare c1 vs c2:", Circle.compare(c1, c2) > 0 ? "c1 lớn hơn" : "c2 lớn hơn");
Static Members (thuộc tính và phương thức tĩnh) thuộc về chính class, không
phải instance. Dùng cho: factory methods, utility functions, constants, Singleton pattern. Bên trong
static method, this trỏ về class, không phải instance.
7. instanceof Under the Hood & Symbol.hasInstance
Toán tử instanceof kiểm tra xem A instanceof B bằng cách đi dọc chuỗi
[[Prototype]] của A và kiểm tra xem B.prototype có xuất hiện ở
đâu đó trong chuỗi không. Đây là tìm kiếm theo tham chiếu, không phải so sánh tên
class.
// instanceof dùng [[Prototype]] chain
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true — Animal.prototype có trong chain
console.log(dog instanceof Object); // true — Object.prototype là cuối chain
// Giả lập instanceof thủ công:
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
console.log(myInstanceof(dog, Animal)); // true
// Symbol.hasInstance: override instanceof behavior
class EvenNumber {
static [Symbol.hasInstance](value) {
return Number.isInteger(value) && value % 2 === 0;
}
}
console.log(2 instanceof EvenNumber); // true
console.log(3 instanceof EvenNumber); // false
console.log(10 instanceof EvenNumber); // true
// ⚠️ instanceof không tin cậy cross-realm (iframe/vm)
// Vì mỗi realm có Object.prototype riêng
// Giải pháp an toàn hơn: Object.prototype.toString.call()
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Array.isArray([])); // true (best practice)
8. Mixin Pattern — Kết Hợp Nhiều Behavior
JavaScript chỉ hỗ trợ đơn kế thừa (single inheritance) — một class chỉ có thể
extends một class khác. Nhưng trong thực tế, một object thường cần nhiều khả năng độc lập
(serialize, emit events, validate...). Mixin là giải pháp: sao chép methods từ nhiều
"nguồn hành vi" vào prototype của class cần dùng.
// Mixin 1: Có thể serialize thành JSON
const SerializableMixin = {
serialize() {
return JSON.stringify(this);
},
static_deserialize(json) {
return Object.assign(new this.constructor(), JSON.parse(json));
}
};
// Mixin 2: Event emitter đơn giản
const EventEmitterMixin = {
_listeners: null,
on(event, fn) {
if (!this._listeners) this._listeners = {};
(this._listeners[event] = this._listeners[event] || []).push(fn);
return this;
},
emit(event, ...args) {
if (this._listeners?.[event]) {
this._listeners[event].forEach(fn => fn(...args));
}
return this;
}
};
// Mixin 3: Validation
const ValidatableMixin = {
validate() {
const errors = [];
if (!this.name || this.name.trim() === '') errors.push('name is required');
if (this.age !== undefined && (this.age < 0 || this.age > 150)) errors.push('age out of range');
return { valid: errors.length === 0, errors };
}
};
// Class dùng nhiều mixin
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// Áp dụng mixins — Object.assign copy methods vào prototype
Object.assign(User.prototype, SerializableMixin, EventEmitterMixin, ValidatableMixin);
const user = new User("Quang", 25);
console.log(user.serialize()); // {"name":"Quang","age":25}
console.log(user.validate()); // { valid: true, errors: [] }
user.on('login', (who) => console.log(who + " đã đăng nhập"));
user.emit('login', user.name); // Quang đã đăng nhập
const invalid = new User("", -5);
console.log(invalid.validate()); // { valid: false, errors: [...] }
9. Object.freeze, Object.seal & structuredClone
JavaScript cung cấp 3 mức độ bảo vệ object khỏi thay đổi:
-
Object.freeze(obj): Ngăn tất cả — không thêm, không xóa, không sửa property. Nhưng chỉ là shallow (1 tầng). -
Object.seal(obj): Chỉ ngăn thêm/xóa property, vẫn cho phép sửa giá trị property đã có. -
structuredClone(obj)(ES2022): Deep copy thực sự — tạo bản sao hoàn toàn độc lập, kể cả nested objects, Map, Set, Date, RegExp.
// 1. Object.freeze — không thể thay đổi gì (shallow)
const config = Object.freeze({ host: "localhost", port: 3000, db: { name: "mydb" } });
config.port = 9999; // silent fail (strict mode: TypeError)
config.newProp = "x"; // silent fail
delete config.host; // silent fail
config.db.name = "hack"; // ⚠️ Vẫn thay đổi được! Chỉ shallow freeze!
console.log(config.port); // 3000 (không đổi)
console.log(config.db.name); // "hack" (đã đổi!)
// Deep freeze cần đệ quy:
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) deepFreeze(obj[key]);
});
return Object.freeze(obj);
}
// 2. Object.seal — có thể sửa giá trị, không thêm/xóa
const user = Object.seal({ name: "An", age: 25 });
user.age = 26; // ✅ OK: sửa giá trị
user.email = "x@x"; // ❌ Fail: thêm property mới
delete user.name; // ❌ Fail: xóa property
console.log(user); // { name: "An", age: 26 }
// 3. structuredClone — deep clone thực sự (ES2022)
const original = {
name: "Test",
nested: { arr: [1, 2, 3] },
date: new Date("2026-01-01")
};
const clone = structuredClone(original);
clone.nested.arr.push(4);
console.log("original:", original.nested.arr.length); // 3 (không ảnh hưởng)
console.log("clone:", clone.nested.arr.length); // 4
Trắc nghiệm 2: instanceof & Prototype
Cho class B extends A {} và const b = new B(). Biểu thức
b instanceof A cho kết quả gì và tại sao?
Trắc nghiệm 3: Object.freeze Shallow
Sau
const obj = Object.freeze({a: 1, nested: {b: 2}}), dòng nào sau đây
thực sự thay đổi được dữ liệu?
Tải file code thực hành minh họa bài học
Chúng tôi cung cấp một file code mẫu hoàn chỉnh chi tiết cách triển khai Prototypal inheritance kiểu cũ, Class hiện đại với thuộc tính private, getter/setter, Mixin pattern và hàm giả lập toán tử 'new':
Tải về class_oop.js
Comments
Bình luận