درود دوستان جاوااسکریپتی! یه برنامه خوب برنامهای هست که توی اون بتونیم کنترل مناسبی روی دادهها، ورودی و خروجیها داشته باشیم. آبجکتها مهمترین و پراستفادهترین نوع داده توی جاوااسکریپت هستن و گاهی اوقات میخوایم کنترل بیشتری روی یک آبجکت خاص داشته باشیم. مثلاً میخوایم خوندن پراپرتیهای آبجکت یا مقدار دادن به اونها رو کنترل کنیم.
یکی از بهترین راههای کنترل یک آبجکت، استفاده از پراکسیها هست. با این ویژگی کارهای جالبی میشه انجام داد که توی ادامه با چند تا از اونها آشنا خواهیم شد. همچنین جالبه که بدونیم توی فریمورک Vue خیلی فراوون از این ویژگی استفاده شده. پراکسی از ES6 وارد جاوااسکریپت شد.
موارد زیر رو توی این قسمت بررسی میکنیم:
پراکسی (Proxy) چیه؟ 🤔
پراکسی آبجکتی هست که میتونه بیشتر اتفاقهایی که برای یک آبجکت دیگه رخ میده رو کنترل کنه. مثلاً خوندن، تغییر مقدار یا حذف یک پراپرتی.
یک پراکسی در واقع یک واسط یا Wrapper هست برای یک آبجکت دیگه تا عملیاتی که برای آبجکت اصلی رخ میده رو مدیریت کنه. مثلاً اگه بخوایم مقدار یک پراپرتی از یک آبجکت رو عوض کنیم، ابتدا این دستور باید از پراکسی عبور کنه تا تغییرات روی آبجکت اصلی اعمال بشه. اینطوری کنترل بیشتری میتونیم روی آبجکت داشته باشیم.
نحوه پراکسی کردن یک آبجکت به صورت زیر هست:
const myProxy = new Proxy(obj, handler);
توی کد بالا آرگومان اول (obj) آبجکتی هست که قصد داریم اون رو مدیریت کنیم. آرگومان دوم که معمولاً با عنوان هندلر (handler) شناخته میشه هم یک آبجکت هست که تنظیمات پراکسی توی اون تعریف میشه.
بهتره با مثال با این ویژگی آشنا بشیم. میخوایم آبجکت user رو پراکسی کنیم:
const user = { name: "Dash" }; const proxiedUser = new Proxy(user, { /* ... */ }); alert(user.name); // Dash alert(proxiedUser.name); // Dash
اینجا آبجکت user پراکسی شد. همونطور که توی خط ۸ میبینیم، با پراکسی هم میتونیم به اعضای آبجکت دسترسی داشته باشیم. ولی با این کد هیچ اتفاقی نخواهد افتاد. چون هندلر خالی هست و هنوز چیزی ننوشتیم که بتونیم آبجکت user رو مدیریت کنیم.
اگه بخوایم وقتی که یک پراپرتی از آبجکت فراخونی میشه رو مدیریت کنیم، توی هندلر از متد get استفاده میکنیم:
const user = { name: "Dash" }; const proxiedUser = new Proxy(user, { get(target, prop) { alert(`${prop} read.`); return target[prop]; } }); alert(proxiedUser.name); // name read. // Dash
برای مشاهده خروجی دکمه ► رو بزنید. همونطور که میبینیم متد get دو پارامتر قبول میکنه. اولی که معمولاً اون رو به اسم target میشناسیم، شامل همون آبجکتی هست که اون رو پراکسی کردیم، یعنی user. پارامتر دوم (prop) شامل اسم پراپرتیای خواهد بود که داره فراخونی میشه.
متد get، یک Trap هست!
Trap چیه؟ 🤔
تِرَپ (Trap) به متدهای مشخصی گفته میشه که توی هندلر تعریف میشه و وظیفه اونها مدیریت کردن اتفاقهای مختلفی هست که برای آبجکت رخ میده. ترپ به زبان پارسی یعنی دام یا تله. اتفاقها قبل از اینکه به آبجکت اصلی برسن، توی تلههای تعریفشده گیر میکنن.
توی مثال بالا ما از ترپ get استفاده کردیم. اگه یک هندلر بدون ترپ get باشه، پراکسی عملیات فراخونی یک پراپرتی رو نمیتونه مدیریت کنه و بنابراین پراپرتی به صورت مستقیم از خود آبجکت خونده میشه.
چه ترپهایی داریم؟
| عنوان | چه زمانی اجرا میشه؟ |
|---|---|
| get() | یک پراپرتی فراخونی میشه |
| set() | هنگام مقدار دادن به یک پراپرتی |
| has() | وقتی عملگر in استفاده میشه |
| deleteProperty() | وقتی عملگر delete استفاده میشه |
| apply() | وقتی یک متد فراخونی میشه |
| construct() | وقتی عملگر new استفاده میشه |
| defineProperty() | وقتی Object.defineProperty استفاده میشه |
| getOwnPropertyDescriptor() | وقتی Object.getOwnPropertyDescriptor استفاده میشه |
| getPrototypeOf() | وقتی Object.getPrototypeOf استفاده میشه |
| isExtensible() | وقتی Object.isExtensible استفاده میشه |
| ownKeys() | وقتی Object.getOwnPropertyNames و Object.getOwnPropertySymbols استفاده میشه |
| preventExtensions() | وقتی Object.preventExtensions استفاده میشه |
| setPrototypeOf() | وقتی Object.setPrototypeOf استفاده میشه |
مثالهای کاربردی
حالا با چند مثال کاربردی این ویژگی رو بررسی میکنیم.
مثال ۱: مقدار پیشفرض یک پراپرتی
در حالت عادی اگه پراپرتیای رو فراخونی کنیم که توی آبجکت وجود نداره، undefined میگیریم. با پراکسی میتونیم چیزی که دلمون میخواد رو برگردونیم:
const obj = {}; const proxy = new Proxy(obj, { get(target, prop) { return prop in target ? target[prop] : null; } }); alert(proxy.ok); // null
مثال ۲: اعتبارسنجی
برای مثال آبجکت user داریم با یک پراپرتی به اسم age و میخوایم قبل از اینکه مقدار اون تنظیم بشه، اون رو اعتبارسنجی کنیم:
let user = { name: "David", age: 3, } user = new Proxy(user, { set(target, prop, value) { if (prop === 'age' && value > 100) { value = 100; } target[prop] = value; return true; } }); user.age = 700; alert(user.age) // 100
توی این کد ۳ نکته داریم.
نکته ۱: اینجا از ترپ set استفاده شده که برای اون ۳ پارامتر تعریف کردیم. پارامتر اول شامل آبجکتی خواهد بود که اون رو پراکسی کردیم، پارامتر دوم عنوان پراپرتیای هست که میخوایم به اون مقدار بدیم و پارامتر سوم مقدار مد نظر ما برای پراپرتی هست.
نکته ۲: ترپ set همیشه باید یک بولین برگردونه. در غیر این صورت خطا میگیریم (توی strict mode). اگه عملیات مقدار دادن با موفقیت انجام شد، مقدار true برمیگردونیم. در غیر این صورت false.
نکته ۳: ما اینجا برای پراکسی کردن آبجکت user، یک آبجکت جدا نساختیم و user رو دوباره مقدار دادیم:
user = new Proxy(user, ...
این کار برای زمانی خوبه که میخوایم بعد از پراکسی کردن یک آبجکت، هیچ رفرنس جدایی از آبجکت اصلی توی برنامه وجود نداشته باشه.
مثال ۳: ایندکسهای منفی آرایهها
توی بعضی از زبانها، وقتی عضوی از آرایه رو با ایندکس منفی میخونیم، میتونیم از آخر به اولِ آرایه پیمایش کنیم. مثلاً با array[-1] میتونیم عضو آخر آرایه رو داشته باشیم. چنین چیزی توی جاوااسکریپت وجود نداره. اما خودمون که میتونیم بسازیمش:
function negativeArray(arr) { return new Proxy(arr, { get(target, index, receiver) { index = +index; return target[index < 0 ? String(target.length + index) : index]; } }) } const a = negativeArray([1, 2, 3, 4, 5]); alert(a[-1]); // 5 alert(a[-2]); // 4 alert(a[0]); // 1
که البته برای این کار هم وجود داره که پشت پرده داره از پراکسی استفاده میکنه.
مثالهای بیشتر از پراکسیها رو میتونید ببنیید. کد زیر یکی از جالبترین مثالهای این ریپازیتوری هست. اگه قبلاً برای جمع دو عدد تابعی مثل add(2, 5) داشتیم، با کد زیر میتونیم تابعهای داینامیکتری مثل add2(5) یا add3(5) داشته باشیم. خطهای ۳۱ تا ۳۴ رو ببینید:
function math() { const baseFns = { add: (a, b) => a + b, sub: (a, b) => a - b, div: (a, b) => a / b, pow: (a, b) => Math.pow(b, a) }; return new Proxy({}, { get: (target, property) => parseFunction(property) }); function parseFunction(property) { const fnType = Object.keys(baseFns).find(fn => property.includes(fn)); const potential = property.replace(fnType, ''); if (potential === '') { return baseFns[fnType]; } const number = Number(potential); return Number.isNaN(number) ? baseFns[fnType] : baseFns[fnType].bind(null, number); } } const { add2, div6, add, pow10 } = math(); alert(add(2, 3)); // 5 alert(add2(3)); // 5 alert(div6(2)); // 3 alert(pow10(2)); // 1024
خب دوستانِ من، این پست هم به آخرش رسید. امیدوارم استفاده کرده باشید 😉✌️