راهنمای جامع برای توسعهدهندگان جهت تسلط بر API پراکسی جاوا اسکریپت. رهگیری و سفارشیسازی عملیات آبجکتها با مثالها، کاربردها و نکات عملکردی.
API پراکسی جاوا اسکریپت: نگاهی عمیق به اصلاح رفتار آبجکتها
در چشمانداز در حال تحول جاوا اسکریپت مدرن، توسعهدهندگان دائماً به دنبال راههای قدرتمندتر و ظریفتری برای مدیریت و تعامل با دادهها هستند. در حالی که ویژگیهایی مانند کلاسها، ماژولها و async/await نحوه کدنویسی ما را متحول کردهاند، یک ویژگی قدرتمند فرابرنامهنویسی (metaprogramming) که در ECMAScript 2015 (ES6) معرفی شد، اغلب کمتر مورد استفاده قرار میگیرد: API پراکسی.
فرابرنامهنویسی ممکن است ترسناک به نظر برسد، اما به سادگی به مفهوم نوشتن کدی است که بر روی کد دیگر عمل میکند. API پراکسی ابزار اصلی جاوا اسکریپت برای این کار است که به شما امکان میدهد یک 'پراکسی' برای آبجکت دیگری ایجاد کنید که میتواند عملیات اساسی آن آبجکت را رهگیری و بازتعریف کند. این مانند قرار دادن یک نگهبان قابل تنظیم در مقابل یک آبجکت است که به شما کنترل کاملی بر نحوه دسترسی و تغییر آن میدهد.
این راهنمای جامع، API پراکسی را رمزگشایی خواهد کرد. ما مفاهیم اصلی آن را بررسی میکنیم، قابلیتهای مختلف آن را با مثالهای عملی تجزیه میکنیم و موارد استفاده پیشرفته و ملاحظات عملکردی را مورد بحث قرار میدهیم. در پایان، شما خواهید فهمید که چرا پراکسیها سنگ بنای فریمورکهای مدرن هستند و چگونه میتوانید از آنها برای نوشتن کدی تمیزتر، قدرتمندتر و قابل نگهداریتر استفاده کنید.
درک مفاهیم اصلی: Target، Handler و Traps
API پراکسی بر سه جزء اساسی بنا شده است. درک نقش آنها کلید تسلط بر پراکسیها است.
- Target (هدف): این آبجکت اصلی است که میخواهید آن را بپوشانید (wrap). میتواند هر نوع آبجکتی باشد، از جمله آرایهها، توابع یا حتی یک پراکسی دیگر. پراکسی این هدف را مجازیسازی میکند و تمام عملیات در نهایت (اگرچه نه لزوماً) به آن ارسال میشود.
- Handler (هندلر): این یک آبجکت است که منطق پراکسی را در خود جای داده است. این یک آبجکت نگهدارنده است که پراپرتیهای آن توابعی به نام 'تله' (traps) هستند. هنگامی که یک عملیات روی پراکسی رخ میدهد، به دنبال تله مربوطه در هندلر میگردد.
- Traps (تلهها): اینها متدهایی در هندلر هستند که دسترسی به پراپرتیها را فراهم میکنند. هر تله با یک عملیات اساسی آبجکت مطابقت دارد. به عنوان مثال، تله
get
خواندن پراپرتی را رهگیری میکند و تلهset
نوشتن پراپرتی را رهگیری میکند. اگر تلهای در هندلر تعریف نشده باشد، عملیات به سادگی به هدف (target) ارسال میشود، گویی که پراکسی وجود ندارد.
سینتکس ایجاد یک پراکسی ساده است:
const proxy = new Proxy(target, handler);
بیایید به یک مثال بسیار ساده نگاه کنیم. ما یک پراکسی ایجاد میکنیم که با استفاده از یک هندلر خالی، به سادگی تمام عملیات را به آبجکت هدف منتقل میکند.
// آبجکت اصلی
const target = {
message: "Hello, World!"
};
// یک هندلر خالی. تمام عملیات به هدف (target) ارسال میشود.
const handler = {};
// آبجکت پراکسی
const proxy = new Proxy(target, handler);
// دسترسی به یک پراپرتی در پراکسی
console.log(proxy.message); // خروجی: Hello, World!
// عملیات به هدف (target) ارسال شد
console.log(target.message); // خروجی: Hello, World!
// تغییر یک پراپرتی از طریق پراکسی
proxy.anotherMessage = "Hello, Proxy!";
console.log(proxy.anotherMessage); // خروجی: Hello, Proxy!
console.log(target.anotherMessage); // خروجی: Hello, Proxy!
در این مثال، پراکسی دقیقاً مانند آبجکت اصلی رفتار میکند. قدرت واقعی زمانی آشکار میشود که شروع به تعریف تلهها در هندلر میکنیم.
آناتومی یک پراکسی: بررسی تلههای (Traps) رایج
آبجکت هندلر میتواند تا ۱۳ تله مختلف داشته باشد که هر کدام با یک متد داخلی اساسی آبجکتهای جاوا اسکریپت مطابقت دارند. بیایید رایجترین و مفیدترین آنها را بررسی کنیم.
تلههای دسترسی به پراپرتی
1. `get(target, property, receiver)`
این مسلماً پرکاربردترین تله است. زمانی فعال میشود که یک پراپرتی از پراکسی خوانده شود.
target
: آبجکت اصلی.property
: نام پراپرتی که به آن دسترسی پیدا شده است.receiver
: خود پراکسی، یا آبجکتی که از آن ارثبری میکند.
مثال: مقادیر پیشفرض برای پراپرتیهای ناموجود.
const user = {
firstName: 'John',
lastName: 'Doe',
age: 30
};
const userHandler = {
get(target, property) {
// اگر پراپرتی در هدف وجود دارد، آن را برگردان.
// در غیر این صورت، یک پیام پیشفرض برگردان.
return property in target ? target[property] : `پراپرتی '${property}' وجود ندارد.`;
}
};
const userProxy = new Proxy(user, userHandler);
console.log(userProxy.firstName); // خروجی: John
console.log(userProxy.age); // خروجی: 30
console.log(userProxy.country); // خروجی: پراپرتی 'country' وجود ندارد.
2. `set(target, property, value, receiver)`
تله set
زمانی فراخوانی میشود که به یک پراپرتی از پراکسی مقداری اختصاص داده شود. این برای اعتبارسنجی، لاگگیری یا ایجاد آبجکتهای فقط-خواندنی (read-only) عالی است.
value
: مقدار جدیدی که به پراپرتی اختصاص داده میشود.- این تله باید یک مقدار boolean برگرداند:
true
اگر تخصیص موفقیتآمیز بود، وfalse
در غیر این صورت (که در حالت strict mode یکTypeError
ایجاد میکند).
مثال: اعتبارسنجی دادهها.
const person = {
name: 'Jane Doe',
age: 25
};
const validationHandler = {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number' || !Number.isInteger(value)) {
throw new TypeError('سن باید یک عدد صحیح باشد.');
}
if (value <= 0) {
throw new RangeError('سن باید یک عدد مثبت باشد.');
}
}
// اگر اعتبارسنجی موفق بود، مقدار را روی آبجکت هدف تنظیم کن.
target[property] = value;
// موفقیت را اعلام کن.
return true;
}
};
const personProxy = new Proxy(person, validationHandler);
personProxy.age = 30; // این معتبر است
console.log(personProxy.age); // خروجی: 30
try {
personProxy.age = 'thirty'; // TypeError ایجاد میکند
} catch (e) {
console.error(e.message); // خروجی: سن باید یک عدد صحیح باشد.
}
try {
personProxy.age = -5; // RangeError ایجاد میکند
} catch (e) {
console.error(e.message); // خروجی: سن باید یک عدد مثبت باشد.
}
3. `has(target, property)`
این تله عملگر in
را رهگیری میکند. این به شما اجازه میدهد کنترل کنید کدام پراپرتیها به نظر میرسد روی یک آبجکت وجود دارند.
مثال: پنهان کردن پراپرتیهای 'خصوصی'.
در جاوا اسکریپت، یک قرارداد رایج این است که پراپرتیهای خصوصی با یک آندرلاین (_) شروع شوند. ما میتوانیم از تله has
برای پنهان کردن آنها از عملگر in
استفاده کنیم.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const hidingHandler = {
has(target, property) {
if (property.startsWith('_')) {
return false; // وانمود کن که وجود ندارد
}
return property in target;
}
};
const dataProxy = new Proxy(secretData, hidingHandler);
console.log('publicKey' in dataProxy); // خروجی: true
console.log('_apiKey' in dataProxy); // خروجی: false (حتی با اینکه در آبجکت هدف وجود دارد)
console.log('id' in dataProxy); // خروجی: true
توجه: این فقط بر عملگر in
تأثیر میگذارد. دسترسی مستقیم مانند dataProxy._apiKey
همچنان کار میکند مگر اینکه شما یک تله get
مربوطه نیز پیادهسازی کنید.
4. `deleteProperty(target, property)`
این تله زمانی اجرا میشود که یک پراپرتی با استفاده از عملگر delete
حذف شود. این برای جلوگیری از حذف پراپرتیهای مهم مفید است.
این تله باید برای حذف موفقیتآمیز true
و برای حذف ناموفق false
برگرداند.
مثال: جلوگیری از حذف پراپرتیها.
const immutableConfig = {
databaseUrl: 'prod.db.server',
port: 8080
};
const deletionGuardHandler = {
deleteProperty(target, property) {
if (property in target) {
console.warn(`تلاش برای حذف پراپرتی محافظتشده: '${property}'. عملیات رد شد.`);
return false;
}
return true; // پراپرتی از قبل وجود نداشت
}
};
const configProxy = new Proxy(immutableConfig, deletionGuardHandler);
delete configProxy.port;
// خروجی کنسول: تلاش برای حذف پراپرتی محافظتشده: 'port'. عملیات رد شد.
console.log(configProxy.port); // خروجی: 8080 (حذف نشد)
تلههای شمارش و توصیف آبجکت
5. `ownKeys(target)`
این تله توسط عملیاتی که لیست پراپرتیهای خود آبجکت را دریافت میکنند، فعال میشود، مانند Object.keys()
، Object.getOwnPropertyNames()
، Object.getOwnPropertySymbols()
و Reflect.ownKeys()
.
مثال: فیلتر کردن کلیدها.
بیایید این را با مثال قبلی پراپرتی 'خصوصی' ترکیب کنیم تا آنها را به طور کامل پنهان کنیم.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const keyHidingHandler = {
has(target, property) {
return !property.startsWith('_') && property in target;
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
},
get(target, property, receiver) {
// همچنین از دسترسی مستقیم جلوگیری کن
if (property.startsWith('_')) {
return undefined;
}
return Reflect.get(target, property, receiver);
}
};
const fullProxy = new Proxy(secretData, keyHidingHandler);
console.log(Object.keys(fullProxy)); // خروجی: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // خروجی: true
console.log('_apiKey' in fullProxy); // خروجی: false
console.log(fullProxy._apiKey); // خروجی: undefined
توجه داشته باشید که ما در اینجا از Reflect
استفاده میکنیم. آبجکت Reflect
متدهایی برای عملیات قابل رهگیری جاوا اسکریپت فراهم میکند و متدهای آن نامها و امضاهای مشابهی با تلههای پراکسی دارند. استفاده از Reflect
برای ارسال عملیات اصلی به هدف، یک بهترین تمرین (best practice) است که تضمین میکند رفتار پیشفرض به درستی حفظ میشود.
تلههای تابع و سازنده
پراکسیها به آبجکتهای ساده محدود نمیشوند. زمانی که هدف یک تابع است، میتوانید فراخوانیها و ساختن نمونهها را رهگیری کنید.
6. `apply(target, thisArg, argumentsList)`
این تله زمانی فراخوانی میشود که یک پراکسی از یک تابع اجرا شود. این تله فراخوانی تابع را رهگیری میکند.
target
: تابع اصلی.thisArg
: کانتکستthis
برای فراخوانی.argumentsList
: لیست آرگومانهای ارسال شده به تابع.
مثال: لاگگیری فراخوانی توابع و آرگومانهای آنها.
function sum(a, b) {
return a + b;
}
const loggingHandler = {
apply(target, thisArg, argumentsList) {
console.log(`فراخوانی تابع '${target.name}' با آرگومانها: ${argumentsList}`);
// اجرای تابع اصلی با کانتکست و آرگومانهای صحیح
const result = Reflect.apply(target, thisArg, argumentsList);
console.log(`تابع '${target.name}' مقدار زیر را برگرداند: ${result}`);
return result;
}
};
const proxiedSum = new Proxy(sum, loggingHandler);
proxiedSum(5, 10);
// خروجی کنسول:
// فراخوانی تابع 'sum' با آرگومانها: 5,10
// تابع 'sum' مقدار زیر را برگرداند: 15
7. `construct(target, argumentsList, newTarget)`
این تله استفاده از عملگر new
روی یک پراکسی از یک کلاس یا تابع را رهگیری میکند.
مثال: پیادهسازی الگوی Singleton.
class MyDatabaseConnection {
constructor(url) {
this.url = url;
console.log(`در حال اتصال به ${this.url}...`);
}
}
let instance;
const singletonHandler = {
construct(target, argumentsList) {
if (!instance) {
console.log('در حال ایجاد نمونه جدید.');
instance = Reflect.construct(target, argumentsList);
}
console.log('بازگرداندن نمونه موجود.');
return instance;
}
};
const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);
const conn1 = new ProxiedConnection('db://primary');
// خروجی کنسول:
// در حال ایجاد نمونه جدید.
// در حال اتصال به db://primary...
// بازگرداندن نمونه موجود.
const conn2 = new ProxiedConnection('db://secondary'); // URL نادیده گرفته خواهد شد
// خروجی کنسول:
// بازگرداندن نمونه موجود.
console.log(conn1 === conn2); // خروجی: true
console.log(conn1.url); // خروجی: db://primary
console.log(conn2.url); // خروجی: db://primary
موارد استفاده عملی و الگوهای پیشرفته
حالا که تلههای جداگانه را پوشش دادیم، بیایید ببینیم چگونه میتوان آنها را برای حل مشکلات دنیای واقعی ترکیب کرد.
۱. انتزاع API و تبدیل دادهها
APIها اغلب دادهها را در فرمتی برمیگردانند که با قراردادهای برنامه شما مطابقت ندارد (مثلاً snake_case
در مقابل camelCase
). یک پراکسی میتواند به طور شفاف این تبدیل را انجام دهد.
function snakeToCamel(s) {
return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}
// تصور کنید این داده خام ما از یک API است
const apiResponse = {
user_id: 123,
first_name: 'Alice',
last_name: 'Wonderland',
account_status: 'active'
};
const camelCaseHandler = {
get(target, property) {
const camelCaseProperty = snakeToCamel(property);
// بررسی کنید که آیا نسخه camelCase مستقیماً وجود دارد
if (camelCaseProperty in target) {
return target[camelCaseProperty];
}
// بازگشت به نام پراپرتی اصلی
if (property in target) {
return target[property];
}
return undefined;
}
};
const userModel = new Proxy(apiResponse, camelCaseHandler);
// اکنون میتوانیم به پراپرتیها با استفاده از camelCase دسترسی پیدا کنیم، حتی اگر به صورت snake_case ذخیره شده باشند
console.log(userModel.userId); // خروجی: 123
console.log(userModel.firstName); // خروجی: Alice
console.log(userModel.accountStatus); // خروجی: active
۲. Observables و اتصال داده (هسته فریمورکهای مدرن)
پراکسیها موتور پشت سیستمهای واکنشی (reactivity) در فریمورکهای مدرن مانند Vue 3 هستند. وقتی یک پراپرتی را در یک آبجکت state پراکسی شده تغییر میدهید، تله set
میتواند برای فعال کردن بهروزرسانیها در UI یا سایر بخشهای برنامه استفاده شود.
در اینجا یک مثال بسیار ساده آورده شده است:
function createObservable(target, callback) {
const handler = {
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
callback(prop, value); // فعال کردن callback در هنگام تغییر
return result;
}
};
return new Proxy(target, handler);
}
const state = {
count: 0,
message: 'Hello'
};
function render(prop, value) {
console.log(`تغییر شناسایی شد: پراپرتی '${prop}' به '${value}' تنظیم شد. در حال رندر مجدد UI...`);
}
const observableState = createObservable(state, render);
observableState.count = 1;
// خروجی کنسول: تغییر شناسایی شد: پراپرتی 'count' به '1' تنظیم شد. در حال رندر مجدد UI...
observableState.message = 'Goodbye';
// خروجی کنسول: تغییر شناسایی شد: پراپرتی 'message' به 'Goodbye' تنظیم شد. در حال رندر مجدد UI...
۳. اندیسهای منفی آرایه
یک مثال کلاسیک و جالب، گسترش رفتار آرایه بومی برای پشتیبانی از اندیسهای منفی است، جایی که -1
به آخرین عنصر اشاره دارد، مشابه زبانهایی مانند پایتون.
function createNegativeArrayProxy(arr) {
const handler = {
get(target, property) {
const index = Number(property);
if (!Number.isNaN(index) && index < 0) {
// تبدیل اندیس منفی به یک اندیس مثبت از انتهای آرایه
property = String(target.length + index);
}
return Reflect.get(target, property);
}
};
return new Proxy(arr, handler);
}
const originalArray = ['a', 'b', 'c', 'd', 'e'];
const proxiedArray = createNegativeArrayProxy(originalArray);
console.log(proxiedArray[0]); // خروجی: a
console.log(proxiedArray[-1]); // خروجی: e
console.log(proxiedArray[-2]); // خروجی: d
console.log(proxiedArray.length); // خروجی: 5
ملاحظات عملکردی و بهترین تمرینها
در حالی که پراکسیها فوقالعاده قدرتمند هستند، اما راهحل جادویی نیستند. درک پیامدهای آنها بسیار مهم است.
سربار عملکردی
یک پراکسی یک لایه غیرمستقیم (indirection) را معرفی میکند. هر عملیات روی یک آبجکت پراکسی شده باید از طریق هندلر عبور کند، که مقدار کمی سربار در مقایسه با یک عملیات مستقیم روی یک آبجکت ساده اضافه میکند. برای اکثر برنامهها (مانند اعتبارسنجی داده یا واکنشگرایی در سطح فریمورک)، این سربار ناچیز است. با این حال، در کدهای حساس به عملکرد، مانند یک حلقه فشرده که میلیونها آیتم را پردازش میکند، این میتواند به یک گلوگاه تبدیل شود. اگر عملکرد یک نگرانی اصلی است، همیشه بنچمارک بگیرید.
قوانین ثابت پراکسی (Invariants)
یک تله نمیتواند به طور کامل در مورد ماهیت آبجکت هدف دروغ بگوید. جاوا اسکریپت مجموعهای از قوانین به نام 'invariants' را اعمال میکند که تلههای پراکسی باید از آنها پیروی کنند. نقض یک invariant منجر به یک TypeError
میشود.
به عنوان مثال، یک invariant برای تله deleteProperty
این است که نمیتواند true
(نشاندهنده موفقیت) را برگرداند اگر پراپرتی مربوطه در آبجکت هدف غیرقابل-تنظیم (non-configurable) باشد. این از ادعای پراکسی مبنی بر حذف پراپرتیای که قابل حذف نیست، جلوگیری میکند.
const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });
const handler = {
deleteProperty(target, prop) {
// این کار invariant را نقض خواهد کرد
return true;
}
};
const proxy = new Proxy(target, handler);
try {
delete proxy.unbreakable; // این یک خطا ایجاد میکند
} catch (e) {
console.error(e.message);
// خروجی: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}
چه زمانی از پراکسیها استفاده کنیم (و چه زمانی نه)
- مناسب برای: ساخت فریمورکها و کتابخانهها (مانند مدیریت state، ORMها)، دیباگ و لاگگیری، پیادهسازی سیستمهای اعتبارسنجی قوی، و ایجاد APIهای قدرتمندی که ساختارهای داده زیربنایی را انتزاعی میکنند.
- جایگزینها را در نظر بگیرید برای: الگوریتمهای حساس به عملکرد، توسعههای ساده آبجکت که یک کلاس یا یک تابع factory کافی است، یا زمانی که نیاز به پشتیبانی از مرورگرهای بسیار قدیمی دارید که از ES6 پشتیبانی نمیکنند.
پراکسیهای قابل ابطال (Revocable)
برای سناریوهایی که ممکن است نیاز به 'خاموش کردن' یک پراکسی داشته باشید (مثلاً به دلایل امنیتی یا مدیریت حافظه)، جاوا اسکریپت Proxy.revocable()
را فراهم میکند. این متد یک آبجکت حاوی هم پراکسی و هم یک تابع revoke
برمیگرداند.
const target = { data: 'sensitive' };
const handler = {};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.data); // خروجی: sensitive
// اکنون، دسترسی پراکسی را باطل میکنیم
revoke();
try {
console.log(proxy.data); // این یک خطا ایجاد میکند
} catch (e) {
console.error(e.message);
// خروجی: Cannot perform 'get' on a proxy that has been revoked
}
پراکسیها در مقابل سایر تکنیکهای فرابرنامهنویسی
قبل از پراکسیها، توسعهدهندگان از روشهای دیگری برای رسیدن به اهداف مشابه استفاده میکردند. درک نحوه مقایسه پراکسیها مفید است.
`Object.defineProperty()`
Object.defineProperty()
یک آبجکت را مستقیماً با تعریف getter و setter برای پراپرتیهای خاص تغییر میدهد. از طرف دیگر، پراکسیها اصلاً آبجکت اصلی را تغییر نمیدهند؛ آنها آن را میپوشانند.
- دامنه: `defineProperty` به صورت هر پراپرتی عمل میکند. شما باید برای هر پراپرتی که میخواهید نظارت کنید، یک getter/setter تعریف کنید. تلههای
get
وset
یک پراکسی سراسری هستند و عملیات را روی هر پراپرتی، از جمله پراپرتیهای جدیدی که بعداً اضافه میشوند، رهگیری میکنند. - قابلیتها: پراکسیها میتوانند طیف وسیعتری از عملیات را رهگیری کنند، مانند
deleteProperty
، عملگرin
، و فراخوانی توابع، که `defineProperty` نمیتواند انجام دهد.
نتیجهگیری: قدرت مجازیسازی
API پراکسی جاوا اسکریپت چیزی فراتر از یک ویژگی هوشمندانه است؛ این یک تغییر اساسی در نحوه طراحی و تعامل ما با آبجکتها است. با اجازه دادن به ما برای رهگیری و سفارشیسازی عملیات اساسی، پراکسیها دری را به روی دنیایی از الگوهای قدرتمند باز میکنند: از اعتبارسنجی و تبدیل یکپارچه دادهها گرفته تا سیستمهای واکنشی که رابطهای کاربری مدرن را قدرت میبخشند.
در حالی که آنها با هزینه عملکردی اندک و مجموعهای از قوانین برای پیروی همراه هستند، توانایی آنها در ایجاد انتزاعهای تمیز، جدا از هم و قدرتمند بینظیر است. با مجازیسازی آبجکتها، میتوانید سیستمهایی بسازید که قویتر، قابل نگهداریتر و گویاتر هستند. دفعه بعد که با یک چالش پیچیده مربوط به مدیریت داده، اعتبارسنجی یا مشاهدهپذیری روبرو شدید، در نظر بگیرید که آیا یک پراکسی ابزار مناسبی برای کار است. ممکن است ظریفترین راهحل در جعبه ابزار شما باشد.