This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Trong lập trình phần mềm, Metaprogramming (Lập trình siêu đối tượng) là kỹ thuật viết mã nguồn có khả năng tự kiểm tra, thay đổi cấu trúc hoặc can thiệp vào hành vi chạy runtime của chính chương trình đó. ES6 mang đến 3 tính năng cốt lõi giúp JavaScript hỗ trợ Metaprogramming mạnh mẽ: Symbol, Proxy và Reflect API.
1. Symbol & Well-known Symbols — Can Thiệp Hành Vi Ngôn Ngữ Cốt Lõi
Symbol là một kiểu dữ liệu nguyên thủy độc bản (unique) và không thể thay đổi. Mỗi khi
bạn gọi Symbol("description"), V8 Engine tạo ra một định danh duy nhất hoàn toàn trong bộ
nhớ, không bao giờ trùng khớp với bất kỳ Symbol nào khác kể cả khi chúng có cùng chuỗi mô tả.
- Ngăn chặn va chạm Key: Symbol cực kỳ hữu dụng làm thuộc tính ẩn khi thiết kế các thư viện dùng chung, tránh việc thuộc tính bị ghi đè ngoài ý muốn bởi mã nguồn của người dùng khác.
-
Well-known Symbols: Là các Symbol hệ thống được định nghĩa sẵn (ví dụ:
Symbol.iteratorđịnh nghĩa giao thức lặp,Symbol.toStringTagtùy chỉnh chuỗi trả về củatoString(),Symbol.toPrimitivetự quyết định kiểu ép kiểu tự động). Chúng cho phép lập trình viên định nghĩa lại (Override) hành vi sâu nhất của ngôn ngữ đối với các đối tượng tự thiết lập.
2. Proxy & Reflect API — Kẻ Gác Cổng Đối Tượng & Chuyển Tiếp Tường Minh
Proxy cho phép bọc một đối tượng khác (gọi là Target) để kiểm soát và
can thiệp (Intercept) vào tất cả các hành vi cơ bản tác động lên đối tượng đó. Handler của Proxy chứa
các phương thức gác cổng gọi là Traps:
get(target, prop): Chặn hành động đọc thuộc tính.-
set(target, prop, value): Chặn hành động ghi thuộc tính (rất mạnh khi dùng để validate dữ liệu đầu vào tự động). -
has(target, prop): Chặn kiểm tra sự tồn tại của thuộc tính thông qua toán tửin(dùng để che giấu các thuộc tính private bắt đầu bằng dấu_). -
deleteProperty(target, prop): Chặn thao tác xóa thuộc tính bằng toán tửdelete(ngăn không cho xóa các khóa quan trọng).
Tại sao luôn phải kết hợp Proxy với Reflect API?
Trong các traps của Proxy, nếu bạn muốn thực hiện hành vi mặc định của đối tượng gốc sau khi đã xử lý
logic riêng, bạn có thể gọi thủ công target[prop] = value. Tuy nhiên, điều này rất dễ gây
lỗi mất ngữ cảnh nếu đối tượng có quan hệ kế thừa hoặc getter/setter phức tạp.
Reflect là một đối tượng tĩnh chứa các phương thức tương ứng khớp 100% với các traps của
Proxy. Dùng Reflect.get(target, prop, receiver) đảm bảo rằng ngữ cảnh
this (receiver) được liên kết chính xác xuyên suốt chuỗi kế thừa đối tượng, loại bỏ hoàn
toàn các lỗi binding ngầm định.
3. Đánh Giá Hiệu Năng Của Proxy Trong V8 Engine
Mặc dù Proxy mang lại khả năng can thiệp cực kỳ linh hoạt (được dùng để xây dựng reactivity trong Vue 3), bạn cần cân nhắc kỹ hiệu năng khi sử dụng nó ở các vòng lặp xử lý dữ liệu lớn (Hot Paths):
- V8 Engine tối ưu hóa việc truy cập thuộc tính của các đối tượng thông thường bằng cách so sánh Shapes (Hidden Classes) và ghi nhớ vị trí ô nhớ thông qua Inline Cache (IC).
- Khi một đối tượng bị bọc bởi Proxy, V8 buộc phải bỏ qua hoàn toàn (Bypass) cơ chế tối ưu hóa Inline Cache. Engine phải thực hiện gọi hàm động (Dynamic Lookup) thông qua các traps handler của Proxy, khiến tốc độ truy cập thuộc tính chậm hơn từ 1.5x đến 5x so với đối tượng thường.
4. Code Thực Hành Nâng Cao: metaprogramming_proxy.js
Mã nguồn mẫu dưới đây minh họa chi tiết cách tạo Symbol độc bản, dùng Symbol.toStringTag đổi kiểu đối
tượng, bọc Proxy để validate tuổi/email, ẩn key private bằng trap has và chặn xóa thuộc
tính bằng trap deleteProperty:
/**
* Bài 5: Metaprogramming trong JavaScript - Symbol, Proxy & Reflect API
* Giáo trình tự học JavaScript - js-tools.org
*/
console.log("=== 1. Symbol & Well-known Symbols (Custom hành vi lõi) ===");
// Khởi tạo các key độc bản tránh va chạm trong Object
const internalId = Symbol("id");
const user = {
[internalId]: 1024,
name: "Quang",
age: 28
};
console.log("Truy cập property thường:", user.name);
console.log("Truy cập Symbol property:", user[internalId]);
// Duyệt qua keys thông thường không hiện Symbol
console.log("Keys thông thường:", Object.keys(user)); // [ 'name', 'age' ]
console.log("Symbol keys:", Object.getOwnPropertySymbols(user)); // [ Symbol(id) ]
// Sử dụng Well-known Symbol: Symbol.toStringTag để sửa đổi mô tả kiểu của Object
class SuperArray {
get [Symbol.toStringTag]() {
return "SuperArray";
}
}
const myArr = new SuperArray();
console.log("Định danh Object.prototype.toString.call(myArr):", Object.prototype.toString.call(myArr)); // [object SuperArray]
console.log("\n=== 2. Proxy & Reflect (Can thiệp & chuyển tiếp thao tác) ===");
// Đối tượng dữ liệu cần bảo vệ và xác thực
const targetUser = {
username: "qtang",
age: 26,
email: "[email protected]"
};
// Khởi tạo Proxy Handler can thiệp các hành vi đọc, ghi, tìm kiếm và xóa dữ liệu
const validatorHandler = {
// Can thiệp hành động đọc (get)
get(target, prop, receiver) {
console.log(`[LOG - Đọc]: Đang truy cập thuộc tính "${prop}"`);
if (!(prop in target)) {
return `Lỗi: Thuộc tính "${prop}" không tồn tại.`;
}
return Reflect.get(target, prop, receiver);
},
// Can thiệp hành động ghi (set)
set(target, prop, value, receiver) {
console.log(`[LOG - Ghi]: Đang cố gắng cập nhật thuộc tính "${prop}" thành`, value);
if (prop === "age") {
if (typeof value !== "number" || value < 0 || value > 120) {
throw new TypeError("Tuổi phải là số nguyên hợp lệ trong khoảng 0-120.");
}
}
if (prop === "email") {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error("Định dạng email không hợp lệ.");
}
}
return Reflect.set(target, prop, value, receiver);
},
// Can thiệp toán tử "in" (has)
has(target, prop) {
console.log(`[LOG - Kiểm tra]: Kiểm tra thuộc tính "${prop}" có tồn tại hay không`);
if (prop.startsWith("_")) {
return false; // Che giấu các biến private bắt đầu bằng "_"
}
return Reflect.has(target, prop);
},
// Can thiệp hành động xóa thuộc tính (deleteProperty)
deleteProperty(target, prop) {
console.log(`[LOG - Xóa]: Đang cố gắng xóa thuộc tính "${prop}"`);
if (prop === "username") {
console.log("[LOG - Chặn]: Không cho phép xóa username!");
return false; // Từ chối xóa
}
return Reflect.deleteProperty(target, prop);
}
};
const proxyUser = new Proxy(targetUser, validatorHandler);
console.log("--- Test đọc thuộc tính ---");
console.log("Đọc username:", proxyUser.username);
console.log("Đọc thuộc tính không có:", proxyUser.address);
console.log("\n--- Test ghi thuộc tính ---");
proxyUser.age = 27;
try {
proxyUser.age = -5; // Lỗi chặn tuổi
} catch (e) {
console.log("Xử lý lỗi chặn tuổi:", e.message);
}
console.log("\n--- Test toán tử \"in\" ---");
targetUser._secretKey = "123456"; // Thêm biến ẩn vào đối tượng gốc
console.log("Tìm '_secretKey' trong proxyUser:", "_secretKey" in proxyUser); // false (bị chặn)
console.log("Tìm 'username' trong proxyUser:", "username" in proxyUser); // true
console.log("\n--- Test xóa thuộc tính ---");
console.log("Xóa email:", delete proxyUser.email); // true
console.log("Xóa username:", delete proxyUser.username); // false (bị chặn)
console.log("\n=== 3. Phân tích hiệu năng Proxy trong V8 ===");
console.log("Proxy tạo ra một wrapper trung gian, bắt buộc V8 Engine phải bypass cơ chế tối ưu hóa Inline Cache (IC).");
console.log("Do đó, truy cập thuộc tính qua Proxy thường chậm hơn 1.5x - 5x so với đối tượng thông thường trong các hot loop.");
5. Playground: Symbol — Unique Keys & Global Registry
Mỗi Symbol là duy nhất dù có cùng mô tả. Symbol.for() tạo Symbol toàn cục có thể chia sẻ
giữa các module:
6. Well-Known Symbols Chuyên Sâu
Các Well-known Symbol cho phép can thiệp vào hành vi cốt lõi của ngôn ngữ — đây là công cụ của người xây dựng framework, không chỉ người dùng:
// 1. Symbol.iterator: làm object iterable (dùng được trong for...of, spread, destructuring)
class Range {
constructor(start, end) { this.start = start; this.end = end; }
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
}
const r = new Range(1, 5);
console.log([...r]); // [1, 2, 3, 4, 5]
for (const n of r) console.log(n); // 1 2 3 4 5
// 2. Symbol.toPrimitive: kiểm soát coercion
class Money {
constructor(amount, currency) { this.amount = amount; this.currency = currency; }
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount;
if (hint === 'string') return `${this.amount} ${this.currency}`;
return this.amount; // 'default'
}
}
const price = new Money(100, 'USD');
console.log(+price); // 100 (number hint)
console.log(`${price}`); // "100 USD" (string hint)
console.log(price + 50); // 150 (default hint)
// 3. Symbol.toStringTag: thay đổi [object ?]
class Queue {
get [Symbol.toStringTag]() { return 'Queue'; }
}
console.log(Object.prototype.toString.call(new Queue())); // [object Queue]
// 4. Symbol.hasInstance: override instanceof
class Even {
static [Symbol.hasInstance](n) { return Number.isInteger(n) && n % 2 === 0; }
}
console.log(4 instanceof Even); // true
console.log(7 instanceof Even); // false
7. Proxy Observable Pattern — Nền Tảng Vue 3 Reactivity
Vue 3 dùng Proxy để tự động theo dõi mọi thay đổi state và trigger re-render. Đây là cách tự xây dựng một reactive store đơn giản:
8. Reflect API — Tại Sao Luôn Dùng Reflect Trong Proxy Trap
Vấn đề khi dùng target[prop] trực tiếp trong Proxy trap: nếu object có getter trong
prototype, this sẽ bị sai. Reflect.get(target, prop, receiver) truyền đúng
receiver (proxy) để getter chạy đúng context:
// Khi base object có getter dùng this:
const base = {
_data: [1, 2, 3],
get first() { return this._data[0]; }
};
// ❌ Sai: target[prop] — this trong getter là target (gốc), không phải proxy
const badProxy = new Proxy(base, {
get(target, prop) {
return target[prop]; // getter "first" chạy với this = target
}
});
// ✅ Đúng: Reflect.get(target, prop, receiver) — this là receiver (proxy)
const goodProxy = new Proxy(base, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver); // getter chạy với this = proxy
}
});
// Thêm interceptor trên proxy
const trackingProxy = new Proxy(base, {
get(target, prop, receiver) {
if (prop !== '_data') console.log(`Accessed: ${String(prop)}`);
return Reflect.get(target, prop, receiver);
}
});
console.log(trackingProxy.first); // log "Accessed: first", "Accessed: _data" → 1
// Reflect methods không throw, trả về boolean
const obj = {};
console.log(Reflect.defineProperty(obj, 'x', { value: 1 })); // true
console.log(Reflect.ownKeys(obj)); // ["x"]
9. WeakMap cho Private State & WeakRef / FinalizationRegistry
WeakMap cho phép lưu private data bên ngoài class mà không gây memory leak — khi object bị GC, data trong WeakMap tự động bị giải phóng. WeakRef và FinalizationRegistry cho phép observe vòng đời object:
// WeakMap: private data bên ngoài class, không lộ qua reflection
const _privateData = new WeakMap();
class BankAccount {
constructor(owner, balance) {
_privateData.set(this, { balance, txHistory: [] });
this.owner = owner;
}
deposit(amount) {
if (amount <= 0) throw new RangeError("Amount must be positive");
const data = _privateData.get(this);
data.balance += amount;
data.txHistory.push({ type: 'deposit', amount, date: new Date().toISOString() });
}
get balance() { return _privateData.get(this).balance; }
get history() { return [..._privateData.get(this).txHistory]; }
}
const acc = new BankAccount("Quang", 1000);
acc.deposit(500);
acc.deposit(250);
console.log("Balance:", acc.balance); // 1750
console.log("History entries:", acc.history.length); // 2
// Không thể truy cập private data từ bên ngoài
console.log("Object.keys:", Object.keys(acc)); // ["owner"]
console.log("Direct access:", _privateData.get(acc).balance); // 1750 (nhưng _privateData là biến module-private)
// So sánh # private fields vs WeakMap:
// # private: đơn giản hơn, nhưng chỉ trong class, không flexible
// WeakMap: flexible hơn, dùng được cho DOM nodes, mixins
Trắc nghiệm 1: Proxy & Inline Cache
Tại sao dùng Proxy trên các đối tượng lớn trong hot loops có thể làm giảm hiệu năng?
Trắc nghiệm 2: Symbol.toPrimitive
Khi bạn dùng +price (unary plus) với object có [Symbol.toPrimitive], hint
nào được truyền vào?
Tải file code thực hành minh họa bài học
File đầy đủ: Symbol, Well-known Symbols, Proxy validator, Observable pattern, Reflect API và WeakMap private state:
Tải về metaprogramming_proxy.js
Comments
Bình luận