Khám phá các mẫu Proxy trong JavaScript để sửa đổi hành vi đối tượng. Tìm hiểu về xác thực, ảo hóa, theo dõi và các kỹ thuật nâng cao khác với ví dụ mã.
Các Mẫu Proxy trong JavaScript: Làm chủ Việc Sửa đổi Hành vi của Đối tượng
Đối tượng Proxy trong JavaScript cung cấp một cơ chế mạnh mẽ để chặn và tùy chỉnh các hoạt động cơ bản trên các đối tượng. Khả năng này mở ra nhiều mẫu thiết kế và kỹ thuật nâng cao để kiểm soát hành vi của đối tượng. Hướng dẫn toàn diện này sẽ khám phá các mẫu Proxy khác nhau, minh họa cách sử dụng chúng bằng các ví dụ mã thực tế.
Proxy trong JavaScript là gì?
Một đối tượng Proxy bao bọc một đối tượng khác (mục tiêu - target) và chặn các hoạt động của nó. Các hoạt động này, được gọi là bẫy (traps), bao gồm tra cứu thuộc tính, gán giá trị, liệt kê và gọi hàm. Proxy cho phép bạn định nghĩa logic tùy chỉnh để thực thi trước, sau, hoặc thay vì các hoạt động này. Khái niệm cốt lõi của Proxy liên quan đến "siêu lập trình" (metaprogramming), cho phép bạn thao tác hành vi của chính ngôn ngữ JavaScript.
Cú pháp cơ bản để tạo một Proxy là:
const proxy = new Proxy(target, handler);
- target: Đối tượng gốc bạn muốn tạo proxy.
- handler: Một đối tượng chứa các phương thức (bẫy) định nghĩa cách Proxy chặn các hoạt động trên đối tượng mục tiêu.
Các Bẫy Proxy Phổ biến
Đối tượng handler có thể định nghĩa nhiều bẫy. Dưới đây là một số bẫy được sử dụng phổ biến nhất:
- get(target, property, receiver): Chặn truy cập thuộc tính (ví dụ:
obj.property
). - set(target, property, value, receiver): Chặn gán thuộc tính (ví dụ:
obj.property = value
). - has(target, property): Chặn toán tử
in
(ví dụ:'property' in obj
). - deleteProperty(target, property): Chặn toán tử
delete
(ví dụ:delete obj.property
). - apply(target, thisArg, argumentsList): Chặn các lệnh gọi hàm (khi mục tiêu là một hàm).
- construct(target, argumentsList, newTarget): Chặn toán tử
new
(khi mục tiêu là một hàm khởi tạo - constructor). - getPrototypeOf(target): Chặn các lệnh gọi đến
Object.getPrototypeOf()
. - setPrototypeOf(target, prototype): Chặn các lệnh gọi đến
Object.setPrototypeOf()
. - isExtensible(target): Chặn các lệnh gọi đến
Object.isExtensible()
. - preventExtensions(target): Chặn các lệnh gọi đến
Object.preventExtensions()
. - getOwnPropertyDescriptor(target, property): Chặn các lệnh gọi đến
Object.getOwnPropertyDescriptor()
. - defineProperty(target, property, descriptor): Chặn các lệnh gọi đến
Object.defineProperty()
. - ownKeys(target): Chặn các lệnh gọi đến
Object.getOwnPropertyNames()
vàObject.getOwnPropertySymbols()
.
Các Mẫu Proxy và Trường hợp sử dụng
Hãy cùng khám phá một số mẫu Proxy phổ biến và cách chúng có thể được áp dụng trong các tình huống thực tế:
1. Xác thực (Validation)
Mẫu Xác thực sử dụng Proxy để thực thi các ràng buộc khi gán thuộc tính. Điều này hữu ích để đảm bảo tính toàn vẹn của dữ liệu.
const validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Tuổi không phải là số nguyên');
}
if (value < 0) {
throw new RangeError('Tuổi phải là một số nguyên không âm');
}
}
// Hành vi mặc định để lưu trữ giá trị
obj[prop] = value;
// Báo hiệu thành công
return true;
}
};
let person = {};
let proxy = new Proxy(person, validator);
proxy.age = 25; // Hợp lệ
console.log(proxy.age); // Output: 25
try {
proxy.age = 'young'; // Gây ra TypeError
} catch (e) {
console.log(e); // Output: TypeError: Tuổi không phải là số nguyên
}
try {
proxy.age = -10; // Gây ra RangeError
} catch (e) {
console.log(e); // Output: RangeError: Tuổi phải là một số nguyên không âm
}
Ví dụ: Hãy xem xét một nền tảng thương mại điện tử nơi dữ liệu người dùng cần được xác thực. Một proxy có thể thực thi các quy tắc về tuổi, định dạng email, độ mạnh mật khẩu và các trường khác, ngăn chặn dữ liệu không hợp lệ được lưu trữ.
2. Ảo hóa (Tải lười - Lazy Loading)
Ảo hóa, còn được gọi là tải lười (lazy loading), trì hoãn việc tải các tài nguyên tốn kém cho đến khi chúng thực sự cần thiết. Một Proxy có thể hoạt động như một trình giữ chỗ cho đối tượng thật, chỉ tải nó khi một thuộc tính được truy cập.
const expensiveData = {
load: function() {
console.log('Đang tải dữ liệu tốn kém...');
// Mô phỏng một hoạt động tốn thời gian (ví dụ: lấy từ cơ sở dữ liệu)
return new Promise(resolve => {
setTimeout(() => {
resolve({
data: 'Đây là dữ liệu tốn kém'
});
}, 2000);
});
}
};
const lazyLoadHandler = {
get: function(target, prop) {
if (prop === 'data') {
console.log('Đang truy cập dữ liệu, tải nếu cần thiết...');
return target.load().then(result => {
target.data = result.data; // Lưu trữ dữ liệu đã tải
return result.data;
});
} else {
return target[prop];
}
}
};
const lazyData = new Proxy(expensiveData, lazyLoadHandler);
console.log('Truy cập ban đầu...');
lazyData.data.then(data => {
console.log('Dữ liệu:', data); // Output: Dữ liệu: Đây là dữ liệu tốn kém
});
console.log('Truy cập tiếp theo...');
lazyData.data.then(data => {
console.log('Dữ liệu:', data); // Output: Dữ liệu: Đây là dữ liệu tốn kém (được tải từ bộ nhớ đệm)
});
Ví dụ: Hãy tưởng tượng một nền tảng mạng xã hội lớn với hồ sơ người dùng chứa nhiều chi tiết và phương tiện liên quan. Tải tất cả dữ liệu hồ sơ ngay lập tức có thể không hiệu quả. Ảo hóa với Proxy cho phép tải thông tin hồ sơ cơ bản trước, sau đó chỉ tải các chi tiết bổ sung hoặc nội dung phương tiện khi người dùng điều hướng đến các phần đó.
3. Ghi nhật ký và Theo dõi (Logging and Tracking)
Proxy có thể được sử dụng để theo dõi các lần truy cập và sửa đổi thuộc tính. Điều này rất có giá trị để gỡ lỗi, kiểm toán và giám sát hiệu suất.
const logHandler = {
get: function(target, prop, receiver) {
console.log(`GET ${prop}`);
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value) {
console.log(`SET ${prop} thành ${value}`);
target[prop] = value;
return true;
}
};
let obj = { name: 'Alice' };
let proxy = new Proxy(obj, logHandler);
console.log(proxy.name); // Output: GET name, Alice
proxy.age = 30; // Output: SET age thành 30
Ví dụ: Trong một ứng dụng chỉnh sửa tài liệu cộng tác, một Proxy có thể theo dõi mọi thay đổi được thực hiện đối với nội dung tài liệu. Điều này cho phép tạo ra một dấu vết kiểm toán, kích hoạt chức năng hoàn tác/làm lại và cung cấp thông tin chi tiết về sự đóng góp của người dùng.
4. Chế độ chỉ đọc (Read-Only Views)
Proxy có thể tạo ra các chế độ xem chỉ đọc của các đối tượng, ngăn chặn các sửa đổi vô tình. Điều này hữu ích để bảo vệ dữ liệu nhạy cảm.
const readOnlyHandler = {
set: function(target, prop, value) {
console.error(`Không thể đặt thuộc tính ${prop}: đối tượng chỉ đọc`);
return false; // Báo hiệu rằng hoạt động set đã thất bại
},
deleteProperty: function(target, prop) {
console.error(`Không thể xóa thuộc tính ${prop}: đối tượng chỉ đọc`);
return false; // Báo hiệu rằng hoạt động delete đã thất bại
}
};
let data = { name: 'Bob', age: 40 };
let readOnlyData = new Proxy(data, readOnlyHandler);
try {
readOnlyData.age = 41; // Gây ra lỗi
} catch (e) {
console.log(e); // Không có lỗi nào được ném ra vì bẫy 'set' trả về false.
}
try {
delete readOnlyData.name; // Gây ra lỗi
} catch (e) {
console.log(e); // Không có lỗi nào được ném ra vì bẫy 'deleteProperty' trả về false.
}
console.log(data.age); // Output: 40 (không thay đổi)
Ví dụ: Hãy xem xét một hệ thống tài chính nơi một số người dùng có quyền truy cập chỉ đọc vào thông tin tài khoản. Một Proxy có thể được sử dụng để ngăn những người dùng này sửa đổi số dư tài khoản hoặc các dữ liệu quan trọng khác.
5. Giá trị mặc định (Default Values)
Một Proxy có thể cung cấp các giá trị mặc định cho các thuộc tính bị thiếu. Điều này giúp đơn giản hóa mã và tránh kiểm tra null/undefined.
const defaultValuesHandler = {
get: function(target, prop, receiver) {
if (!(prop in target)) {
console.log(`Không tìm thấy thuộc tính ${prop}, trả về giá trị mặc định.`);
return 'Giá trị mặc định'; // Hoặc bất kỳ giá trị mặc định thích hợp nào khác
}
return Reflect.get(target, prop, receiver);
}
};
let config = { apiUrl: 'https://api.example.com' };
let configWithDefaults = new Proxy(config, defaultValuesHandler);
console.log(configWithDefaults.apiUrl); // Output: https://api.example.com
console.log(configWithDefaults.timeout); // Output: Không tìm thấy thuộc tính timeout, trả về giá trị mặc định. Giá trị mặc định
Ví dụ: Trong một hệ thống quản lý cấu hình, một Proxy có thể cung cấp các giá trị mặc định cho các cài đặt bị thiếu. Ví dụ, nếu một tệp cấu hình không chỉ định thời gian chờ kết nối cơ sở dữ liệu, Proxy có thể trả về một giá trị mặc định đã được xác định trước.
6. Siêu dữ liệu và Chú thích (Metadata and Annotations)
Proxy có thể đính kèm siêu dữ liệu hoặc chú thích vào các đối tượng, cung cấp thông tin bổ sung mà không sửa đổi đối tượng gốc.
const metadataHandler = {
get: function(target, prop, receiver) {
if (prop === '__metadata__') {
return { description: 'Đây là siêu dữ liệu cho đối tượng' };
}
return Reflect.get(target, prop, receiver);
}
};
let article = { title: 'Giới thiệu về Proxy', content: '...' };
let articleWithMetadata = new Proxy(article, metadataHandler);
console.log(articleWithMetadata.title); // Output: Giới thiệu về Proxy
console.log(articleWithMetadata.__metadata__.description); // Output: Đây là siêu dữ liệu cho đối tượng
Ví dụ: Trong một hệ thống quản lý nội dung, một Proxy có thể đính kèm siêu dữ liệu vào các bài viết, chẳng hạn như thông tin tác giả, ngày xuất bản và từ khóa. Siêu dữ liệu này có thể được sử dụng để tìm kiếm, lọc và phân loại nội dung.
7. Chặn Hàm (Function Interception)
Proxy có thể chặn các lệnh gọi hàm, cho phép bạn thêm logic ghi nhật ký, xác thực hoặc các logic tiền/hậu xử lý khác.
const functionInterceptor = {
apply: function(target, thisArg, argumentsList) {
console.log('Đang gọi hàm với các đối số:', argumentsList);
const result = target.apply(thisArg, argumentsList);
console.log('Hàm trả về:', result);
return result;
}
};
function add(a, b) {
return a + b;
}
let proxiedAdd = new Proxy(add, functionInterceptor);
let sum = proxiedAdd(5, 3); // Output: Đang gọi hàm với các đối số: [5, 3], Hàm trả về: 8
console.log(sum); // Output: 8
Ví dụ: Trong một ứng dụng ngân hàng, một Proxy có thể chặn các lệnh gọi đến các hàm giao dịch, ghi nhật ký mỗi giao dịch và thực hiện kiểm tra phát hiện gian lận trước khi thực hiện giao dịch.
8. Chặn Hàm khởi tạo (Constructor Interception)
Proxy có thể chặn các lệnh gọi hàm khởi tạo, cho phép bạn tùy chỉnh việc tạo đối tượng.
const constructorInterceptor = {
construct: function(target, argumentsList, newTarget) {
console.log('Đang tạo một thể hiện mới của', target.name, 'với các đối số:', argumentsList);
const obj = new target(...argumentsList);
console.log('Thể hiện mới đã được tạo:', obj);
return obj;
}
};
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let ProxiedPerson = new Proxy(Person, constructorInterceptor);
let person = new ProxiedPerson('Alice', 28); // Output: Đang tạo một thể hiện mới của Person với các đối số: ['Alice', 28], Thể hiện mới đã được tạo: Person { name: 'Alice', age: 28 }
console.log(person);
Ví dụ: Trong một framework phát triển game, một Proxy có thể chặn việc tạo ra các đối tượng game, tự động gán ID duy nhất, thêm các thành phần mặc định và đăng ký chúng với engine game.
Những Lưu ý Nâng cao
- Hiệu suất: Mặc dù Proxy mang lại sự linh hoạt, chúng có thể gây ra chi phí hiệu suất. Điều quan trọng là phải đo lường và phân tích mã của bạn để đảm bảo rằng lợi ích của việc sử dụng Proxy lớn hơn chi phí hiệu suất, đặc biệt là trong các ứng dụng yêu cầu hiệu suất cao.
- Tính tương thích: Proxy là một sự bổ sung tương đối mới cho JavaScript, vì vậy các trình duyệt cũ hơn có thể không hỗ trợ chúng. Sử dụng tính năng phát hiện hoặc polyfill để đảm bảo khả năng tương thích với các môi trường cũ hơn.
- Proxy có thể thu hồi: Phương thức
Proxy.revocable()
tạo ra một Proxy có thể bị thu hồi. Việc thu hồi một Proxy sẽ ngăn chặn bất kỳ hoạt động nào tiếp theo bị chặn. Điều này có thể hữu ích cho mục đích bảo mật hoặc quản lý tài nguyên. - Reflect API: API Reflect cung cấp các phương thức để thực hiện hành vi mặc định của các bẫy Proxy. Việc sử dụng
Reflect
đảm bảo rằng mã Proxy của bạn hoạt động nhất quán với đặc tả của ngôn ngữ.
Kết luận
Proxy trong JavaScript cung cấp một cơ chế mạnh mẽ và linh hoạt để tùy chỉnh hành vi của đối tượng. Bằng cách làm chủ các mẫu Proxy khác nhau, bạn có thể viết mã mạnh mẽ, dễ bảo trì và hiệu quả hơn. Cho dù bạn đang triển khai xác thực, ảo hóa, theo dõi hay các kỹ thuật nâng cao khác, Proxy đều cung cấp một giải pháp linh hoạt để kiểm soát cách các đối tượng được truy cập và thao tác. Luôn xem xét các tác động về hiệu suất và đảm bảo khả năng tương thích với các môi trường mục tiêu của bạn. Proxy là một công cụ quan trọng trong kho vũ khí của nhà phát triển JavaScript hiện đại, cho phép các kỹ thuật siêu lập trình mạnh mẽ.
Khám phá thêm
- Mozilla Developer Network (MDN): JavaScript Proxy
- Exploring JavaScript Proxies: Bài viết trên Smashing Magazine