الكائن Proxy يُحيط كائن آخر و يتوسط العمليات التى تُجري على هذا الكائن، مثل القراءة أو التعديل على الخصائص وغيرها والتعامل معهم بشكل اختياري أو تجعل الكائن الأساسي يتعامل معهم دون تدخل.
إن الـProxies مُستخدمة فى كثير من المكتبات وبعض أطر العمل. سنرى الكثير من التطبيقات العملية فى هذا المقال.
الوسيط Proxy
الشكل:
let proxy = new Proxy(target, handler);
target– هو الكائن الذي يتم إحاطته ويمكن أن يكون أى شيئ حتي الدوال (functions).handler– إعدادت الـproxy: هو كائن يحتوي علي “traps” والتي هي عبارة عن دوال تعمل فى العمليات. مثلًا الـgettrap لقراءة خاصية من الـobjecttargetوكذلكsettrap لتعديل\إضافة خاصية للـobjecttargetوهكذا.
بالنسبة للعمليات فى الـ proxy، فإنه إذا كان هناك trap فى الـobject handler وثم بعد ذلك تعمل وبعد ذلك يكون لديها الفرصة للتعامل معه وإذا لم يوجد trap فإن العملية تُجري علي target.
كمثال مبدأى، هيا ننشئ proxy بدون traps:
let target = {};
let proxy = new Proxy(target, {}); // handler فارغ
proxy.test = 5; // التعديل علي البروكسي (1)
alert(target.test); // 5, ظهور الخاصية فى الكائن الأصلي!
alert(proxy.test); // 5, ويمكننا قرائته من البروكسي أيضًا (2)
for (let key in proxy) alert(key); // test, التكرار يعمل (3)
وبما أنه لا توجد traps فإن كل العمليات التي التي تُجرى على الـ proxy يتم تحويلها إلى الـ target.
- عملية التعديل
proxy.test=تُعدل القيمة فى الـtarget. - عملية القراءة
proxy.testتقوم بإرجاع القيمة منtarget. - التكرار على الـ
proxyيقوم بإرجاع القيم منtarget.
كما نري فإن الـproxy بدون traps هو محيط شفاف حول target. أى أنه لا يفعل أى شيئ فى المنتصف.
Proxyإن الـ كائن من نوع خاص لا يحتوي علي أى خصائص وعندما يكون handler فارغًا فإنه يحوّل كل العمليات إلى target.
للحصول علي قدرات أكثر هيا نضيف traps.
ماذا يمكننا أن نتدخّل (intercept) بهم؟
لأغلب العمليات علي الـobjects توجد دوال تسمي “الدوال الداخلية” “internal method” فى مصدر جافا سكريبت والتي تصف كيفية عملها عند أقل مستوي. علي سبيل المثال الدالة [[Get]] هي دالة داخلية لقراءة خاصية والدالة [[Set]] هي دالة داخلية لإضافة\تعديل خاصية وهكذا. هذه الدوال تستخدم فقط داخليًا ولا يمكننا استعمالهم بشكل مباشر.
تتدخّل الـ Proxy traps فى استدعاء هذه الدوال. وهم موجودون بالتفصيل في المصدر وفي الجدول أدناه.
لكل دالة داخلية يوجد trap في هذا الجدول: اسم الدالة التي يمكننا إضافتها للمتغير الذي يسمي handler والذي نضيفه للـ new Proxy للتدخل فى العملية:
| Internal Method | Handler Method | Triggers when… |
|---|---|---|
[[Get]] |
get |
reading a property |
[[Set]] |
set |
writing to a property |
[[HasProperty]] |
has |
in operator |
[[Delete]] |
deleteProperty |
delete operator |
[[Call]] |
apply |
function call |
[[Construct]] |
construct |
new operator |
[[GetPrototypeOf]] |
getPrototypeOf |
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries |
تفرض جافا سكريبت بعض الثوابت – شروط يجب أن تتحقق بالmethods و الtraps.
أغلبهم لإرجاع قيم:
- الدالة
[[Set]]يجب أن تقوم بإرجاعtrueإذا كُتبت القيمة بشكل صحيح أو تقوم بإرجاعfalseإذا لم تكن كذلك. - الدالة
[[Delete]]يجب أن اقةم بإرجاعtrueإذا تم حذف القيمة بشكل صحيح وإلا تقوم بإرجاعfalse. - …وهكذا، وسنري المزيد في الأمثلة القادمة.
هناك المزيد من الثوابت، مثلًا:
- الدالة
[[GetPrototypeOf]]الموجودة في البروكسي، يجب أن تُرجع نفس القيمة التي تُرجعها الدالة[[GetPrototypeOf]]الموجودة في الأوبجكت المستهدف (target). أو يمكننا القول بطريقة أخري، أن استرجاع القيم من الprototype الخاص بالبروكسي يجب دائما أن تُرجع الprototype الخاص بالأوبجكت المستهدف (target).
تستطيع الtraps أن تعترض طريق هذه العمليات، ولكن يجب أن يتبعو هذه القواعد.
تضمن الثوابت صحة وتناسق سلوك مزايا اللغة. وللإطلاع علي القائمة الكاملة للثوابت فهي موجودة في المصدر. لا تقلق، لن تقوم بمخالفة هذه الثوابت مالم تكن تقوم بعمل غريب.
هيا نري كيف يتم تطبيق ذلك في أمثلة عملية.
إرجاع قيمة افتراضية بالtrap “get”
أكثر الtraps استعمالًا التي تسترجع أو تعدل الخصائص (properties).
لاعتراض طريق عملية الإسترجاع، يجب أن يحتوي الhandler علي الدالة get(target, property, receiver).
يتم تشغيلها عندما تكون الخاصية للإسترجاع بالمتغيرات التالية:
target– هو الأوبجكت المستهدف والذي يتم تمريره كمتغير أول للnew Proxy,property– اسم الخاصية,receiver– إذا كانت الخاصية للإسترجاع getter فإن الreceiverهو الأوبجكت الذي سيتم استخدامه كقيمة لthisعند استرجاعها. وعادة مايكون البروكسي نفسه (أو أوبجكت يرث منه). والآن لا نريد هذا المتغير، ولذلك سيتم شرحه لاحقًا بالتفصيل.
هيا نستخدم get لتطبيق القيمة الإفتراضية لأوبجكت.
سنقوم بإنشاء array تحتوي علي أرقام والتي تقوم بإرجاع 0 للقيم الغير موجودة.
عادة، عندما يحاول أحد أن يسترجع قيمة غير موجودة في الarray فإنه يحصل علي undefined, ولكننا سنحيط الarray العادية ببروكسي والذي سيعترض عملية الاسترجاع ويقوم بإرجاع 0 إذا لم توجد الخاصية:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // القيمة الإفتراضية
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (لا يوجد عنصر كهذا)
كما نري، فأن هذا يمكن تحقيقه بسهوله باستخدام الtrap get.
يمكننا استخدام Proxy لتطبيق أي طريقة للقيم الإفتراضية.
تخيل أن لدينا قاموسًا، بالجمل وترجماتها:
let dictionary = {
Hello: 'مرحبًا',
Bye: 'إلي اللقاء',
};
alert(dictionary['Hello']); // مرحبًا
alert(dictionary['Welcome']); // undefined
حاليًا، ليست هناك جملة، فعند الإسترجاع من القاموس تقوم بإرجاع undefined. ولكن في التطبيق العملي، فإن ترك الجملة غير مترجمة أفضل من undefined. ولذلك سنجعلها تقوم بإرجاع الجملة غير مترجمة بدلًا من undefined.
لتحقيق ذلك، سنحيط الdictionary ببروكسي والذي سيقوم باعتراض طريق استرجاع الخصائص:
let dictionary = {
'Hello': 'مرحبًا',
'Bye': 'إلي اللقاء'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // تعترض طريق استرجاع الخصائص من القاموس
if (phrase in target) { // إذا كانت لدينا بالفعل
return target[phrase]; // قم بإرجاع الترجمة
} else {
// وإلا فلتقم بإرجاع الجملة كما هي
return phrase;
}
}
});
// قم باكتشاف الجمل الاعتباطية!
// في أسوأ الحالت لن يتم ترجمتهم.
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (لا توجد ترجمة)
لاحظ كيف يستبدل البروكسي المتغير:
dictionary = new Proxy(dictionary, ...);
يجب أن يقوم المتغير باستبدال الأوبجكت المستهدف\المستقبل بشكل تام. يجب ألا يستطيع أحد استدعاء الأوبجكت المستقبل بعد أن تمت إحاطته ببروكسي. وإلا سيكون من السهل أن تفسد كل شيئ.
التحقق من القيم باستخدام الtrap “set”
دعنا نقول أننا نريد array للأرقام فقط. وإذا تمت إضافة قيمة من نوع آخر، يجب أن يكون هناك خطأ.
تعمل الtrap set عند التعديل علي خاصية.
set(target, property, value, receiver):
target– هو الأوبجكت المستقبل، هو الذي يتم تمريره كمتغير أول لـnew Proxy,property– إسم الخاصية,value– قيمة الخاصية,receiver– شبيه بالJtrapget، ولكن مفيد فقط للخصائص التي يتم التعديل عليها.
يجب أن يقوم الtrap set بإرجاع true إذا نجح التعديل، وإلا يقوم بإرجاع false (يقوم بتشغيل TypeError).
هيا نستخدمه للتحقق من القيم الجديدة:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // لاعتراض عملية التعديل
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // تمت إضافته بنجاح
numbers.push(2); // تمت إضافته بنجاح
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' on proxy returned false)
alert("لن يتم الوصول إلي هذا السطر أبدا، فهناك خطأ فى السطر الأعلي");
لاحظ أن: الوظيفة الأساسية للarray ما زالت تعمل كما هي! تُضاف القيم باستخدام push. وتزداد الخاصية length تلقائيًا عند إضافة قيم جديدة. لا يعدل البروكسي أي شيئ.
لسنا مضطرين لاستبدال الدوال المسؤولة عن إضافة قيم للarray مثل push و unshift, وهكذا, لإضافة تحققات هناك, لأنهم ضمنيًا يستخدمون الدالة [[Set]] والتي يتم اعتراضها بالبروكسي.
وهكذا يكون الكود نظيف ومتناسق.
trueكما قيل بالأعلي، هناك ثوابت يجب الإلتزام بها.
ففي الدالة set، يجب أن تقوم بإرجاع true في التعديل الناجح.
إذا نسينا أن نفعل ذلك أو قمنا بإرجاع أى قيمة غير حقيقية (falsy)، يتقوم العملية بتفعيل الخطأ TypeError.
التكرار باستخدام “ownKeys” و “getOwnPropertyDescriptor”
التكرارات Object.keys, for..in وأغلب الدوال الأخري اللتي تقوم بالتكرار علي خصائص الأوبجكت تستخدم ضمنيًا الدالة [[OwnPropertyKeys]] (والتي يتم اعتراضها عن طريق الtrapownKeys) لاسترجاع قائمة من الخصائص.
دوال كهذه تختلف فى التفاصيل:
- تقوم الدالة
Object.getOwnPropertyNames(obj)بإرجاع الخصائص التي ليست من نوع الرمز (symbol). - تقوم الدالة
Object.getOwnPropertySymbols(obj)بإرجاع الخصائص من نوع الرمز. - تقوم الدالة
Object.keys/values()بإرجاع الخصائص والقيم التي ليست من نوع الرمز والتي تحتوى علي المعرٌفenumerable(تم شرح المعرفات في المقال رايات الخصائص و واصفاتها). - التكرارات
for..inتقوم بالتكرار علي الخصائص التي ليست من نوع الرمز والمحتوية علي المعرفenumerableوأيضًا الخصائص الموجودة فى الprototype.
…ولكن جميعهم يبدأون بهذه القائمة.
في المثال أدناه استخدمنا الtrap ownKeys لجعل التكرار for..in علي الأوبجكت user، وكذلك Object.keys و Object.values، لتخطي الخصائص اللتي تبدأ بـ _:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" filters out _password
for(let key in user) alert(key); // name, then: age
// same effect on these methods:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
إنها تعمل الآن.
علي الرغم من ذلك، إذا قمنا بإرجاع خاصية ليست موجودة في الأوبجكت فإن Object.keys لن تعرضه:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <فارغ>
لماذا؟ السبب بسيط: تقوم Object.keys بإرجاع الخصائص المحتويه علي المعرف enumerable فقط. للتحقق من ذلك، هي تقوم باستدعاء الدالة [[GetOwnProperty]] لكل خاصية لاسترجاع المعرف الخاص بها. وهنا، بما أنه لا يوجد خصائص، فإن معرفها فارغ، ولا يوجد المعرف enumerable فيتم تخطيها.
لجعل Object.keys تقوم بإرجاع خاصية، نحتاج إلي أن تكون موجودة في الأوبجكت ومحتوية علي المعرف enumerable، أو يمكننا اعتراض استدعاء الدالة [[GetOwnProperty]] (يقوم بهذا الtrap getOwnPropertyDescriptor)، ويقوم بإرجاع واصف (descriptor) والراية enumerable: true.
هنا مثال علي ذلك:
let user = {};
user = new Proxy(user, {
ownKeys(target) {
// يتم استدعاؤها مرة لإرجاع قائمة
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) {
// يتم استدعاؤها لكل خاصية
return {
enumerable: true,
configurable: true,
/* ...other flags, probable "value:..." */
};
},
});
alert(Object.keys(user)); // a, b, c
هيا نسجل ذلك مرة أخري: نحتاج لاعتراض [[GetOwnProperty]] فقط إذا كانت الخاصية غير موجودة في الأوبجكت.
الخصائص المحمية باستخدام “deleteProperty” وغيره من الtraps
هناك شئ شائع متفق عليه وهو أن الخصائص التي تبدأ بـ_ هي ضمنية ولا يجب أن يتم الوصول إليها من خارج الأوبجكت.
وهذا ممكن تقنيًا:
let user = {
name: 'John',
_password: 'secret',
};
alert(user._password); // secret
هيا نستخدم الproxies لمنع أي وصول إلي الخسائص البادئة بـ _.
سنحتاج إلي الtraps:
getلإطهار خطأ عند استرجاع خاصية كهذه,setلإظهار خطأ عند التعديل,deletePropertyلإظهار خطأ عند الحذف,ownKeysلاستثناء الخصائص البادئة بـ_من التكرارfor..inوالدوال الأخري مثلObject.keys.
إليك الكود:
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // لاعتراض التعديل علي الخاصية
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // to intercept property deletion
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // لاعتراض عرض الخصائص في قائمة
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" لا تسمح بإرجاع _password
try {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// "set" لا تسمح بتعديل _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// "deleteProperty" لا تسمج بحذف _password
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// "ownKeys" تستثني _password
for(let key in user) alert(key); // name
لاحظ التفصيلة المهمه في الtrap get في السطر (*):
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
لماذا نحتاج إلي دالة لاستدعاء value.bind(target)؟
والسبب أن دوال أﻷوبجكت، مثل user.checkPassword()، يجب أن تقدر علي الوصول إلى _password:
user = {
// ...
checkPassword(value) {
// دالة الأوبجكت يجب أن تقدر علي الوصول إلي _password
return value === this._password;
},
};
استدعاء user.checkPassword() يقوم بإرجاع user المُحاط ببروكسي كقيمة لـ this (الأوبجكت قبل علامة النقطة هو قيمة this)، ولذلك فعندما تحاول الوصول إلي this._password ينشط الـtrap get (تعمل مع كل استدعاء لخاصية) وتظهر خطأًا.
A call to user.checkPassword() gets proxied user as this (the object before dot becomes this), so when it tries to access this._password, the get trap activates (it triggers on any property read) and throws an error.
So we bind the context of object methods to the original object, target, in the line (*). Then their future calls will use target as this, without any traps.
هذا الحل عادة ما يعمل، ولكنه ليس مثاليًا، فإن دالة كهذه يمكنها أن ترجع الأوبجكت غير محاط ببروكسي في أي مكان آخر وهكذا سيفسد كل شيئ: أين الأوبجكت الأصلي؟ وأين المحاط ببروكسي؟
إلي جانب ذلك، فإن أوبجكت كهذا يمكن إحاطته ببروكسي أكثر من مره (كل بروكسي يمكن أن يضيف تعديلات غير منتهية للأوبجكت)، وإذا قمنا بتمرير أوبجكت غير محاط لأوبجكت، فإنه يمكن أن يكون هناك نتائج غير متوقعه.
ولذلك فإن بروكسي كهذا لا يجب أن يتم استخدامه في كل مكان.
محركات جافا سكريبت الحديثة تدعم الخصائص الخاصة (private properties) في الكلاس، مسبوقة بالعلامة #, تم شرحهم في المقال