الكائن Proxy
يُحيط كائن آخر و يتوسط العمليات التى تُجري على هذا الكائن، مثل القراءة أو التعديل على الخصائص وغيرها والتعامل معهم بشكل اختياري أو تجعل الكائن الأساسي يتعامل معهم دون تدخل.
إن الـProxies مُستخدمة فى كثير من المكتبات وبعض أطر العمل. سنرى الكثير من التطبيقات العملية فى هذا المقال.
الوسيط Proxy
الشكل:
let proxy = new Proxy(target, handler);
target
– هو الكائن الذي يتم إحاطته ويمكن أن يكون أى شيئ حتي الدوال (functions).handler
– إعدادت الـproxy: هو كائن يحتوي علي “traps” والتي هي عبارة عن دوال تعمل فى العمليات. مثلًا الـget
trap لقراءة خاصية من الـobjecttarget
وكذلكset
trap لتعديل\إضافة خاصية للـ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) في الكلاس، مسبوقة بالعلامة #
, تم شرحهم في المقال الخواص والدوال الـ `private` و الـ `protected`. لا نحتاج إلي بروكسي.
خصائص كهذه لها مشاكلها الخاصة. تحديدًا، لا يمكن توارثها.
“In range” مع الtrap “has”
هيا نري أمثلة أخري.
لدينا الأوبجكت range:
let range = {
start: 1,
end: 10,
};
نود أن نستعمل in
للتحقق من وجود هذا الرقم في الـrange
.
الtrap has
الذي يعترض اسدعاء in
.
has(target, property)
target
– هو الأوبجكت المستهدف، يتم تمريره كمتغير أول لـnew Proxy
,property
– اسم الخاصية
هنا التطبيق:
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
مصطلح بديل لطيف، أليس كذلك؟ وسهل تطبيقه.
الدوال المُحاطة: "apply"
يمكن أن نحيط دالة ببروكسي أيضًا.
الtrap apply(target, thisArg, args)
يتعامل مع استدعاء البروكسي كدالة:
target
هو الأوبجكت المستهدف (الدوال ماهي إلا أوبجكت في جافا سكريبتthisArg
هو قيمةthis
.args
هو قائمة من المتغيرات.
علي سبيل المثال هيا نعيد استذكار delay(f, ms)
، والتي قمنا بإنشائها في المقال المزخرفات decorators والتمرير forwarding: التابعان call وapply.
في هذا المقال أنشأناها بدون بروكسي. فإن استدعاء delay(f, ms)
قام بإرجاع دالة تفوض كل الإستدعاءات إلي f
بعد ms
مللي ثانيه.
هنا الكود السابق، من غير بروكسي:
function delay(f, ms) {
// تُرجع غلاف يقوم بتمرير الاستدعاء ﻹلي f بعد انتهاء الوقت
return function () {
// (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// بعد هذه الإحاطه فإن استدعاء الدالة سيتأخر ل 3 ثواني
sayHi = delay(sayHi, 3000);
sayHi('John'); // Hello, John! (after 3 seconds)
هذا يعمل كما رأينا بالفعل. الدالة المُحيطة (*)
تقوم بالإستدعاء بعد انتهاء الوقت.
ولكن لا تقوم الدالة المحيطه بتمرير قراءة أو تعديل خاصية أو أي شيء آخر. بعد الإحاطه، تتم خسارة الوصول إلي الخاصائص الخاصة بالدالة الأصليه، مثل name
, length
وغيرهم:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (function length is the arguments count in its declaration)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments)
إن ال Proxy
أقوي بكثير لأنه يقوم بتمرير كل شيئ إلي الأوبجكت المستهدف.
هيا نستخدم بروكسي بلًا من الدالة المحيطة:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target
sayHi("John"); // Hello, John! (after 3 seconds)
النتيجة مماثله، ولكن الآن ليست الاستدعاءات فقط مايتم تمريرها ولكن الكل العمليات أيضًا. ولذلك فإن sayHi.length
يتم استرجاعها بشكل صحيح بعد الإحاطه في السطر (*)
.
لدينا غلاف أقوي.
هناك traps أخري: القائمة الكاملة موجودة في بداية المقال. وطريقة استخدامهم مشابهة لما سبق.
الأوبجكت Reflect
الأوبجكت Reflect
هو أوبجكت موجود في اللغة والذي يقوم بتبسيط إنشاء Proxy
.
لقد قيل سابقًا أن الدوال الضمنية مثل [[Get]]
, [[Set]]
وغيرهم هم دوال مصدرية فقط، لا يمكن استدعاؤهم بشكل مباشر.
الكائن Reflect
يجعل ذلك بسيطا نوعا ما. الدوال الخاصة به هي غلاف مباشر للدوال الضمنية.
هنا أمثلة لبعض العمليات وكذلك استدعاءات ال Reflect
والتي تقوم بعمل نفس الشيئ:
Operation | Reflect call |
Internal method |
---|---|---|
obj[prop] |
Reflect.get(obj, prop) |
[[Get]] |
obj[prop] = value |
Reflect.set(obj, prop, value) |
[[Set]] |
delete obj[prop] |
Reflect.deleteProperty(obj, prop) |
[[Delete]] |
new F(value) |
Reflect.construct(F, value) |
[[Construct]] |
… | … | … |
علي سبيل المثال:
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
بالتحديد، يسمح لنا الReflect
باستدعاء العمليات (new
, delete
…) كدوال (Reflect.construct
, Reflect.deleteProperty
, …). وهذه ميزة جيدة ومثيرة، ولكن هنا شيئ آخر مهم.
لكل خاصية ضمنية، تم اعتراضها ببروكسي، دالة في ال Reflect
، بنفس الإسم والمتغيرات الخاصة بالtrap.
لذلك يمكننا استخدام Reflect
لتمرير عملية إلي الكائن الأصلي.
في هذا المثال، كلا من get
و set
يقومان بتمرير القراءة والتعديل إلي الأوبجكت بشكل شفاف، ويظهران رسالة:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"
هنا:
Reflect.get
تقرأ خاصية لأوبجكت.Reflect.set
تقوم بتعديل خاصية لأوبجت وتُرجعtrue
في حالة النجاح وfalse
في غير ذلك.
وهكذا كل شيئ بسيط: إذا أراد trap أن يمرر استدعاءًا لأوبجكت فإنه من الكافي استدعاء Reflect.<method>
بنفس الخصائص.
في أغلب الحالات يمكننا فعل نفس الشيئ بدون Reflect
، علي سبيل المثال، قراءة خاصية بـ Reflect.get(target, prop, receiver)
يمكن استبداله بـ target[prop]
. ولكن مع ذلك هناك فروق مهمه.
إحاطة الـgetter أو الجالب ببروكسي
هيا نري مثالًا يوضح لماذا Reflect.get
أفضل. وسنري أيضًا لماذا get/set
لديهم المتغير الرابع receiver
الذي لم نستخدمه من فيل.
لدينا الأوبجكت user
الذي يحتوي علي الخاصية _name
وجالب لها.
هنا بروكسي حولها:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // Guest
الtrap get
شفاف هنا، حيث تقوم بإرجاع الخاصية الأصلية ولا تفعل أي شيئ آخر. وهذا طافٍ لمثالنا.
كل شيئ يبدو كأنه صحيح. ولكن هيا ننشئ مثالًا أكثر تعقيدًا.
بعد وراثة أوبجكت آخر admin
من user
، يمكننا مشاهدة السلوك الخاطئ:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // outputs: Guest (?!?)
استرجاع admin.name
يجب أن ينتج “Admin”, وليس
"Guest"`!
ماذا حدث؟ من الممكن أننا فعلنا شيئًا خاطئًا مع الوراثة؟
ولكن إذا قمنا بإزالة البروكسي، سيعمل كل شيئ كما هو متوقع.
المشكلة تحديدًا في البروكسي في السطر (*)
.
-
عند قراءة
admin.name
فإن الأوبجكتadmin
ليس لديه خاصية كهذه فيذهب البحث إلي الـprototype المتصل به. -
الprototype هو
userProxy
. -
عند قراءة الخاصية
name
من البروكسي، فإن الtrapget
يُشغل ويُرجع قيمتها من الأوبجكت ألأصلي كماtarget[prop]
في السطر(*)
.استدعاء
target[prop]
، عندما تكونprop
جالبة، تقوم بتشغيل الكود في سياقthis=target
. لذلك تكون النتيجةthis._name
من الكائن الأصليtarget
والذي هوuser
.
لإصلاح ذلك، نحتاج إلي receiver
، المتغير الثالث للtrap get
. هي تحافظ علي القيمة الصحيحة لـ this
وتمريرها لجالب. في حالتنا هو admin
.
كيف تمرر سياق لجالب؟ في الدوال العادية يمكننا استخدام call/apply
ولكن هذا جالب ولا يتم استدعاؤه، فقط الوصول إليه إليه.
تستطيع Reflect.get
أن تفعل ذلك. كل شيئ يمكنه أن يعمل بشكل صحيح إذا استخدمناه.
هنا الكود المصحح:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
والآن فإن receiver
الذي يحافظ علي القيمة الصحيحه this
، يتم تمريره للجالب باستخدام Reflect.get
في السطر (*)
.
يمكننا كتابة الtrap بشكل أقصر:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
استدعاءات Reflect
مسماة بنفس أسماء الtraps وتستقبل نفس المتغيرات. تم إنشائهم بهذه الطريقة.
لذلك فإن return Reflect...
يعطينا طريقة آمنو لتمرير العمليات دون أن نقلق إن كنا نسينا شيئًا.
حدود البروكسي
إن البروكسي هو طريقة فريدة لتعديل سلوك الكائنات الموجودة علي أدني مستوي. ومع ذلك هو ليس أفضل شيئ. هناك حدود.
الأوبجكتس الموجود بالفعل: Internal slots
الكثير من الكائنات الموجودة بالفعل مثل Map
, Set
, Date
, Promise
وغيرهم يستخدمون مايسمي “internal slots”.
هي عبارة عن خصائص، ولكن محفوظة ويتم استخدامها ضمنيًا فقط. علي سبيل المثال، يخزن الـ Map
العناصر في فتحة داخلية (internal slot) تسمي [[MapData]]
. الدوال الموجودة في اللغة تصل إليهم مباشرة وليس عن طريق [[Get]]/[[Set]]
. ولذلك فإن البروكسي لا يستطيع اعتراضهم.
لماذا نهتم؟ إنهم أشياء مضمنة علي كل الأحوال!
حسنًا، هنا المشكله، بعد أن يتم إحاطة أوبجكت كهذا ببروكسي فإن البروكسي لا يملك هذه الـ internal slots ولذلك فإن الدوال الضمنية ستفشل.
علي سبيل المثال:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // خطأ
ضمنيًا، يخزن الـ Map
كل البيانات في [[MapData]]
. والبروكسي ليس لديه فتحة (slot) كهذه. والدالة Map.prototype.set
تحاول أن تصل إلي الخاصية الداخلية this.[[MapData]]
ولكن بما أن this=proxy
فإنها لا تجدها بداخل البروكسي وتفشل.
لحسن الحظ، هناك طريقة لإصلاح ذلك:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)
والآن هي تعمل جيدًا، لأن الtrap get
يربط خصائص الدالة ، مثل map.set
، بالأوبجكت المستهدف.
علي عكس المثال السابق، فإن قيمة this
بداخل proxy.set(...)
لن تكون بروكسي ولكن فقط الmap
الأصلي. لذلك عندما تحاول الدالة set
أن تصل إلي this.[[MapData]]
فإنها تنجح.
Array
لا تحتوي علي internal slotsلذلك لا توجد مشكلة كهذه عند إحاطة المصفوفة ببروكسي.
الخصائص الخاصة Private fields
A similar thing happens with private class fields.
علي سبيل المثال، الدالة getName()
تصل إلي الخاصية الخاصة #name
وتقف بعد الإحاطة:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // خطأ
السبب في ذلك أن الخصائص الداخلية يتم إنشاؤها بالـ internal slots. ولا تستخدم جافا سكريبت [[Get]]/[[Set]]
عند الوصول إليهم.
عند استدعاء getName()
فإن قيمة this
يتم إحاطتها بالأوبجكت user
، وهي لا تملك فتحة (slot) مع الخصائص الخاصة.
مرة أخري، فإن الحل بالربط يجعلها تعمل:
class User {
#name = 'Guest';
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
},
});
alert(user.getName()); // Guest
كما قيل، فإن الحل له عيوب، كما تم التوضيح سابقًا: فإنه يقوم بتعريض الكائن الأصلي للدالة ويسمح بتمريره وإنهاء الكائنات بروكسي الأخري.
البروكسي ليس هو الأوبجكت المستهدف
إن البروكسي والكائن الأصلي مختلفان. هذا طبيعي، صحيح؟
لذلك إذا استخدمنا الأوبدجكت الأصلي كخاصية، ثم إحاطته ببروكسي، فإن البروكسي لا يمكن إيجاده:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
كما نري، فإنه بعد الإحاطة لا نستطيع أن نجد user
في المجموعه allUsers
، لأن البروكسي هو كائن مختلف.
===
ولكن ليست هناك طريقة لاعتراض اختبار المساواة ===
للأوبجكتس. فإن الأوبجكت مساوٍ تماما لنفسه فقط وليس أي قيمة أخري.
لذلك فإن كل العمليات والكلاسز المبنيو في اللغة والتي تقارن الكائنات للمساواة ستقوم بالتفريق بين الأوبجكت والبروكسي. لا يوجد استبدال هنا.
البروكسي القابل للإلغاء
دعنا نقول أن لدينا مصدر، ونريد أن نمنع الوصول إليه في أى وقت.
مانستطيع فعله هو أن نحيطه ببروكسي قابل للإلغاء، بدون أي trap. هذا البروكسي سيقوم بتمرير العمليات إلي الأوبجكت ويمكننا أن نمنع الوصول إليه في أى وقت.
الشكل:
let {proxy, revoke} = Proxy.revocable(target, handler)
الإستدعاء يقوم بإرجاع أوبجكت يحتوي علي proxy
ودالة revoke
لإبطاله.
هاك مثالًا:
let object = {
data: 'Valuable data',
};
let { proxy, revoke } = Proxy.revocable(object, {});
// تمرير اليروكسي إلى مكان آخر بلًا من الأوبجكت...
alert(proxy.data); // بيانات قيمة
// فيما بعد
revoke();
// لا يعمل البروكسي الآن (تم الإلغاء)
alert(proxy.data); // خطأ
A call to revoke()
removes all internal references to the target object from the proxy, so they are no longer connected.
Initially, revoke
is separate from proxy
, so that we can pass proxy
around while leaving revoke
in the current scope.
We can also bind revoke
method to proxy by setting proxy.revoke = revoke
.
Another option is to create a WeakMap
that has proxy
as the key and the corresponding revoke
as the value, that allows to easily find revoke
for a proxy:
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ..somewhere else in our code..
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // خطأ (تم إلغاؤه)
We use WeakMap
instead of Map
here because it won’t block garbage collection. If a proxy object becomes “unreachable” (e.g. no variable references it any more), WeakMap
allows it to be wiped from memory together with its revoke
that we won’t need any more.
المراجع
الملخص
إن الـ Proxy
هو غلاف حول الأوبجكت، والذي يقوم بتمرير العمليات إلي الأوبجكت، ويقوم باعتراض بعضهم بشكل اختياري.
يمكنه أن يحيط أي أوبجكت، بما فيه الكلاس والدالة.
الشكل:
let proxy = new Proxy(target, {
/* traps */
});
…بعد ذلك يجب أن نستخدم proxy
في كل مكان بدلًا من target
. إن البروكسي لا يحتوي علي خصائص أو دوال خاصة به. هو يقوم باعتراض العمليه إذا وجد trap وإلا فإنه يمرر العمليه إلي الأوبجكت المستهدف.
يمكننا أن نعترض:
We can trap:
- Reading (
get
), writing (set
), deleting (deleteProperty
) a property (even a non-existing one). - Calling a function (
apply
trap). - The
new
operator (construct
trap). - Many other operations (the full list is at the beginning of the article and in the docs).
هذا يسمح لنا أن ننشئ خواص ودوال افتراضية واسترجاع قيم افتراضية وأوبجكت ملحوظ والكثير.
يمكننا أيضًا أن نعترض أوبجكت مرات عدة ببروكسي مختلف، وتعليمها بطرق مختلفة.
The Reflect API is designed to complement Proxy. For any Proxy
trap, there’s a Reflect
call with same arguments. We should use those to forward calls to target objects.
البروكسي له حدود:
- الأوبجكت المبنية بالفعل تمتلك “internal slots”، والوصول إليها لا مككن إحاطته ببروكسي. أنظر إلي الحل أعلاه.
- ومثله أيضًا الخصائص الخاصة في الكلاس، حيث أنهم يتم إنشاؤهم داخليا باستخدام فتحات (slots). ولذلك فإن الدوال المغلفة يجب أن تحتوي علي الأوبجكت المستهدف كقيمة لـ
this
للوصول إليهم بنجاح. - اختبار التساوي
===
لا يمكن اعتراضه. - السرعة: هذا يعتمد علي الإنجن ولكن بشكل عام فإن الوصول إلي خاصية ببروطسي بسيط يستغرق وقتًا أطول.