يغلّف كائن الوكيل Proxy
كائنًا آخر ويعترض عملياته مثل: خاصيات القراءة أو الكتابة وغيرهما. ويعالجها اختياريًا بمفرده، أو يسمح بشفافية للكائن التعامل معها بنفسه.
تستخدم العديد من المكتبات وأُطر عمل المتصفح الوسطاء. وسنرى في هذا الفصل العديد من تطبيقاته العملية.
الوسيط Proxy
صياغته:
let proxy = new Proxy(target, handler)
-
target
-- وهو الكائن الّذي سنغلِّفه يمكن أي يكون أي شيء بما في ذلك التوابع. -
handler
-- لإعداد الوسيط: وهو كائن يحتوي على "الاعتراضات"، أي دوالّ اعتراض العمليات. مثل: الاعتراضget
لاعتراض خاصية القراءة في الهدفtarget
، الاعتراضset
لاعتراض خاصية الكتابة في الهدفtarget
، وهكذا.
سيراقب الوسيط العمليات فإن كان لديه اعتراض مطابق في المعالج handler
للعملية المنفذة عند الهدف، فعندها سينفذ الوسيط هذه العملية ويعالجها وإلا ستُنفذ هذه العملية من قِبل الهدف نفسه.
لنأخذ مثالًا بسيطًا يوضح الأمر، لننشئ وسيطًا بدون أي اعتراضات:
let target = {}; let proxy = new Proxy(target, {}); // المعالج فارغ proxy.test = 5; // الكتابة على الوسيط (1) alert(target.test); // النتيجة: 5، ظهرت الخاصية في كائن الهدف ! alert(proxy.test); // النتيجة: 5، يمكننا قرائتها من الوسيط أيضًا (2) for(let key in proxy) alert(key); // النتيجة: test, عملية التكرار تعمل (3)
انطلاقًا من عدم وجود اعتراضات سيُعاد توجيه جميع العمليات في الوسيط proxy
إلى كائن الهدف target
.
-
تحدد عملية الكتابة
proxy.test=
القيمة عند الهدفtarget
، لاحظ السطر (1). -
تعيد عملية القراءة
proxy.test
القيمة من عند الهدفtarget
. لاحظ السطر (2). -
تعيد عملية التكرار على الوسيط
proxy
القيم من الهدفtarget
. لاحظ السطر (3).
كما نرى، الوسيط proxy
في هذه الحالة عبارة عن غِلاف شفاف حول الكائن الهدف target
.
لنُضف بعض الاعتراضات من أجل تفعيل المزيد من الإمكانيات.
ما الذي يمكننا اعتراضه؟
بالنسبة لمعظم العمليات على الكائنات، هناك ما يسمى "الدوالّ الداخلية" في مواصفات القياسية في اللغة، والّتي تصف كيفية عمل الكائن عند أدنى مستوى. فمثلًا الدالة الداخلية [[Get]]
لقراءة خاصية ما، والدالة الداخلية [[Set]]
لكتابة خاصية ما، وهكذا. وتستخدم هذه الدوال من قبل المواصفات فقط، ولا يمكننا استدعاؤها مباشرة من خلال أسمائها.
اعتراضات الوسيط تتدخلّ عند استدعاء هذه الدوالّ. وهي مدرجة في المواصفات القياسية للوسيط وفي الجدول أدناه.
يوجد اعتراض مخصص (دالّة معالجة) لكل دالّة داخلية في هذا الجدول: يمكننا إضافة اسم الدالّة إلى المعالج handler
من خلال تمريرها كوسيط new Proxy
لاعتراض العملية.
الدالة الداخلية | الدالة المعالجة | ستعمل عند |
---|---|---|
[[Get]]
|
get
|
قراءة خاصية |
[[Set]]
|
set
|
الكتابة على خاصية |
[[HasProperty]]
|
has
|
التأكد من وجود خاصية ما in
|
[[Delete]]
|
deleteProperty
|
عملية الحذف delete
|
[[Call]]
|
apply
|
استدعاء تابِع |
[[Construct]]
|
construct
|
عملية الإنشاء -الباني- new
|
[[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.getOwnPropertyDescriptorfor..in Object.keys/values/entries
|
[[OwnPropertyKeys]]
|
ownKeys
|
Object.getOwnPropertyNames Object.getOwnPropertySymbols for..in Object/keys/values/entries
|
تحذير: التعامل مع الثوابت
تفرض علينا لغة جافاسكربت بعض الثوابت - الشروط الواجب تحقيقها من قِبل الدوالّ الداخلية والاعتراضات.
معظمها للقيم المُعادة من الدوالّ:
-
يجب أن تعيد الدالّة
[[Set]]
النتيجةtrue
إذا عُدلت القيمة بنجاح، وإلا ستُعيدfalse
. -
يجب أن تعيد الدالّة
[[Delete]]
النتيجةtrue
إذا حُذفت القيمة بنجاح، وإلا ستُعيدfalse
. - …وهكذا، سنرى المزيد من الأمثلة لاحقًا.
هنالك بعض الثوابت الأخرى، مثل:
-
يجب أن تُعيد الدالة
[[GetPrototypeOf]]
المطبقة على كائن الوسيط نفس القيمة الّتي ستُعيدها الدلّة[[GetPrototypeOf]]
المطبقة على كائن الهدف لكائن الوسيط، بتعبير آخر، يجب أن تعرض دائمًا قراءة النموذج الأولي لكائن الوسيط نفس قراءة النموذج الأولي لكائن الهدف.
يمكن للاعتراضات التدخل في هذه العمليات ولكن لابد لها من اتباع هذه القواعد.
تضمن هذه الثوابت السلوك الصحيح والمُتسق لمميّزات اللغة. وجميع هذه الثوابت موجودة في المواصفات القياسية للغة. وغالبًا لن تكسرَهم إن لم تنفذّ شيئًا غريبًا.
لنرى كيف تعمل في مثال عملي.
إضافة اعتراض للقيم المبدئية
تعدُّ خاصيات القراءة / الكتابة من أكثر الاعتراضات شيوعًا.
لاعتراض القراءة، يجب أن يكون لدى المعالج handler
دالّة get(target, property, receiver)
.
وتُشغّل عند قراءة خاصية ما، وتكون الوسطاء:
-
target
- هو كائن الهدف، والذي سيمرر كوسيط أول لِـnew Proxy
، -
property
- اسم الخاصية، -
receiver
- إذا كانت الخاصية المستهدفة هي الجالِب (getter)، فإنreceiver
هو الكائن الذي سيُستخدم على أنه بديل للكلمة المفتاحيةthis
في الاستدعاء. عادةً ما يكون هذا هو كائنproxy
نفسه (أو كائن يرث منه، في حال ورِثنا من الوسيط). لا نحتاج الآن هذا الوسيط، لذلك سنشرحها بمزيد من التفصيل لاحقًا.
لنستخدم الجالِب get
لجلب القيم الافتراضية لكائن ما.
سننشئ مصفوفة رقمية تُعيد القيمة 0
للقيم غير الموجودة.
عادةً عندما نحاول الحصول على عنصر من مصفوفة، وكان هذا العنصر غير موجود، سنحصل على النتيجة غير معرّف undefined
، لكننا هنا سنُغلف المصفوفة العادية داخل الوسيط والّذي سيعترضُ خاصية القراءة ويعيد 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 (هذا العنصر غير موجود)
كما رأينا، من السهل جدًا تنفيذ ذلك باعتراض الجالِب get
.
يمكننا استخدام الوسيط proxy
لتنفيذ أي منطق للقيم "الافتراضية".
تخيل أن لدينا قاموسًا (يربط القاموس مفاتيح مع قيم على هيئة أزواج) يربط العبارات مع ترجمتها:
let dictionary = { 'Hello': 'Hola', 'Bye': 'Adiós' }; alert( dictionary['Hello'] ); // Hola alert( dictionary['Welcome'] ); // undefined
مبدئيًا إن حاولنا الوصول إلى عبارة غير موجودة في القاموس فسيُعيد غير معرّف undefined
، ولكن عمليًا إن ترك العبارة بدون مترجمة أفضل من undefined
، لذا لنجعلها تُعيد العبارة بدون مترجمة بدلاً من undefined
.
لتحقيق ذلك، سنغلّف القاموس dictionary
بالوسيط ليعترض عمليات القراءة:
let dictionary = { 'Hello': 'Hola', 'Bye': 'Adiós' }; 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, ...);
يجب على الوسيط استبدال كائن الهدف بالكامل في كل مكان. ولا يجب أن يشير أي شيء للكائن الهدف بعد أن يستبدلهُ الوسيط. وإلا فذلك سيُحدثُ فوضى عارمة.
تدقيق المدخلات باعتراض الضابِط "set"
لنفترض أننا نريد إنشاء مصفوفة مخصصة للأرقام فقط. وإن أضيفت قيمة من نوع آخر، يجب أن يظهر خطأ.
يُشغّل اعتراض الضابِط set
عند الكتابة على الخاصية.
set(target, property, value, receiver)
-
target
- الكائن الهدف، الذي سنمرره كوسيط أولnew Proxy
، -
property
- اسم الخاصية، -
value
- قيمة الخاصية، -
receiver
- مشابه للدالّةget
، ولكن هنا لضبط الخاصيات فقط.
يجب أن يُعيد الاعتراض 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' في الوسيط أعادت القيمة false) alert("This line is never reached (error in the line above)");
لاحظ أن الدوال المدمجة للمصفوفات ما تزال تعمل! إذ تضاف القيم عن طريق push
. فتزداد خاصية length
تلقائيًا عند إضافة هذه القيم. لذا فإن الوسيط لن يكسر أي شيء.
يجب علينا ألا نعيد كتابة الدوالّ المُدمجة للمصفوفات الّتي تضيف القيم مثل push
وunshift
، وما إلى ذلك، إن كان هدفنا إضافة عمليات تحقق، لأنهم يستخدمون داخليًا دالّة [[Set]]
والّتي ستُعترضُ من قِبل الوسيط.
إذن الشيفرة نظيفة ومختصرة.
لا تنسَ أن تعيد القيمة true
عند نجاح عملية الكتابة. كما ذكر أعلاه، هناك ثوابت ستعقّد. بالنسبة للضابط set
، يجب أن يُرجع true
عند نجاح عملية الكتابة. إذا نسينا القيام بذلك أو إعادة أي قيمة زائفة، فإن العملية تؤدي إلى خطأ TypeError
.
التكرار باستخدام تابع "ownKeys" و"getOwnPropertyDescriptor"
إن الدالة Object.keys
وحلقة التكرار for..in
ومعظم الطرق الأخرى الّتي تستعرض خاصيات الكائن، وتستخدم الدالّة الداخلية [[OwnPropertyKeys]]
للحصول على قائمة بالخاصيات يمكننا اعتراضها من خلال ownKeys
.
تختلف هذه الدوال وحلقات التكرار على الخاصيات تحديدًا في:
-
Object.getOwnPropertyNames (obj)
: تُعيد الخاصيات غير الرمزية. -
Object.getOwnPropertySymbols (obj)
: تُعيد الخاصيات الرمزية. -
Object.keys/values()
: تعيد الخاصيات/القيم غير الرمزية والتي تحمل راية قابلية الاحصاءenumerable
(شرحنا في مقال سابق ما هي رايات الخواص وواصفاتها يمكنك الاطلاع عليه لمزيد من التفاصيل). -
حلقات
for..in
: تمرّ على الخاصيات غير الرمزية التي تحمل راية قابلية الاحصاءenumerable
، وكذلك تمرّ على خاصيات النموذج الأولي (prototype).
… لكن كلهم يبدأون بهذه القائمة.
في المثال أدناه ، نستخدم اعتراض 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" سيزيل الخاصّية _password for(let key in user) alert(key); // name, then: age // نفس التأثير سيحدث على هذه التوابع: 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) ); // <empty>
هل تسأل نفسك لماذا؟ السبب بسيط: تُرجع الدالّة Object.keys
فقط الخاصيات الّتي تحمل راية قابلية الإحصاء enumerable
. وهي تحقق من رايات الخاصيات لديها باستدعاء الدالّة الداخلية [[GetOwnProperty]]
لكل خاصية للحصول على واصِفها. وهنا، نظرًا لعدم وجود الخاصّية، فإن واصفها فارغ، ولا يوجد راية قابلية الإحصاء enumerable
، وبناءً عليه تخطت الدالّة الخاصّية.
من أجل أن ترجع الدالّة Object.keys
الخاصية، فيجب أن تكون إما موجودة في الكائن الأصلي، وتحمل راية قابلية الإحصاء enumerable
، أو يمكننا وضع اعتراض عند استدعاء الدالة الداخلية [[GetOwnProperty]]
(اعتراض getOwnPropertyDescriptor
سيحقق المطلوب)، وإرجاع واصف لخاصية قابلية الإحصاء بالإيجاب هكذا enumerable: true
.
إليك المثال ليتوضح الأمر:
let user = { }; user = new Proxy(user, { ownKeys(target) { // يستدعى مرة واحدة عند طلب قائمة الخاصيات return ['a', 'b', 'c']; }, getOwnPropertyDescriptor(target, prop) { // تستدعى من أجل كلّ خاصّية return { enumerable: true, configurable: true /* ...يمكننا إضافة رايات أخرى مع القيم المناسبة لها..." */ }; } }); alert( Object.keys(user) ); // a, b, c
نعيد مرة أخرى: نضيف اعتراض [[GetOwnProperty]]
إذا كانت الخاصّية غير موجودة في الكائن الأصلي.
الخاصيات المحمية والاعتراض "deleteProperty" وغيره
هناك إجماع كبير في مجتمع المطورين بأن الخاصيات والدوالّ المسبوقة بشرطة سفلية _
هي للاستخدام الداخلي. ولا ينبغي الوصول إليها من خارج الكائن.
هذا ممكن تقنيًا:
let user = { name: "John", _password: "secret" }; // لاحظ أن في الوضع الطبيعي يمكننا الوصول لها alert(user._password); // secret
لنستخدم الوسيط لمنع الوصول إلى الخاصيات الّتي تبدأ بشرطة سفلية _
.
سنحتاج استخدام الاعتراضات التالية:
-
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) { // لاعتراض عملية حذف الخاصّية 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
لاحظ التفاصيل المهمة في الاعتراض 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
، سينُشّط الاعتراض get
( والّذي يُشغلّ عند قراءة خاصية ما) ويلقي الخطأ.
لذلك نربط سياق الدوال الخاصة بالكائن مع دوالّ الكائن الأصلي، أيّ الكائن الهدف target
لاحظ السطر (*)
. بعد ذلك، الاستدعاءات المستقبلية ستستخدم target
بدلّ this
، دون الاعتراضات.
هذا الحل ليس مثاليًا ولكنه يفي بالغرض، ولكن يمكن لدالّةٍ ما أن تُمرّر الكائن غير المغَلّف بالوسيط (أي الكائن الأصلي) إلى مكان آخر، وبذلك ستخرّب الشيفرةولن نستطع الإجابة على أسئلة مثل: أين يستخدم الكائن الأصلي؟ وأين يستخدم الكائن الوسيط؟
أضف إلى ذلك، يمكن للكائن أن يغلف بأكثر من وسيط (تضيف عدة وسطاء "تعديلات" مختلفة على الكائن الأصلي)، وإن مرّرنا كائن غير مغلّف بالوسيط إلى دالّة ما، ستكون هناك عواقب غير متوقعة.
لذا، لا ينبغي استخدام هذا الوسيط في كل الحالات.
ملاحظة: الخاصيات الخاصة بالصنف
تدعم محركات جافاسكربت الحديثة الخاصيات الخاصّة بالأصناف، وتكون مسبوقة بـ #
. تطرقنا لها سابقًا في مقال الخصائص والتوابع الخاصّة والمحمية. وبدون استخدام أيّ الوسيط.
على الرغم من ذلك هذه الخاصيات لها مشاكلها الخاصة أيضًا. وتحديدًا، مشكلة عدم إمكانية توريث هذه الخاصيات.
استخدام الاعتراض "In range" مع "has"
لنرى مزيدًا من الأمثلة.
لدينا الكائن range
:
let range = { start: 1, end: 10 };
نريد استخدام المعامل in
للتحقق من أن الرقم موجود في range
.
إن الاعتراض 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"
يمكننا أيضًا تغليف دالّة ما باستخدام كائن الوسيط.
يعالج الاعتراض apply(target, thisArg, args)
استدعاء كائن الوسيط كتابع:
-
target
: الكائن الهدف (التابع - أو الدالّة - هي كائن في لغة جافا سكربت)، -
thisArg
: قيمةthis
. -
args
: قائمة الوسطاء.
فمثلًا، لنتذكر المُزخرف delay(f, ms)
، الذي أنشأناه في مقال المزخرفات والتمرير.
في تلك المقالة أنشأناه بدون استخدام الوسيط. عند استدعاء الدالة delay(f, ms)
ستُعيد دالّة أخرى، والّتي بدورها ستوجه جميع الاستدعاءات إلى f
بعد ms
ملّي ثانية.
إليك التطبيق المثال بالاعتماد على التوابع:
function delay(f, ms) { // يعيد المغلف والذي بدوره سيوجه الاستدعاءات إلى f بعد انتهاء مهلة زمنية معينة return function() { // (*) setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(user) { alert(`Hello, ${user}!`); } // بعد عملية التغليف استدعي الدالّة sayHi بعد 3 ثواني sayHi = delay(sayHi, 3000); sayHi("John"); // Hello, John! (بعد 3 ثواني)
كما رأينا، غالبًا ستعمل هذه الطريقة وفق المطلوب. تُنفذّ الدالّة المغلفة في السطر (*)
استدعاءً بعد انتهاء المهلة.
لكن دالة المغلف لا تعيد توجيه خاصيات القراءة أو الكتابة للعمليات أو أيّ شيء آخر. بعد عملية التغليف، يُفقدُ إمكانية الوصول لخاصيات التوابع الأصلية، مثل: name
وlength
وغيرها:
function delay(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(user) { alert(`Hello, ${user}!`); } alert(sayHi.length); // 1 (طول الدالة هو عدد الوسطاء في تعريفها) sayHi = delay(sayHi, 3000); alert(sayHi.length); // 0 (إن عدد وسطاء عند تعريف المغلّف هو 0)
في الحقيقة إن إمكانيات الوسيط Proxy
أقوى بكثير من ذلك، إذ إنه يعيد توجيه كل شيء إلى الكائن الهدف.
لنستخدم الوسيط 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 (*) سيعيد الوسيط توجيه عملية "get length" إلى الهدف sayHi("John"); // Hello, John! (بعد 3 ثواني)
نلاحظ أن النتيجة نفسها، ولكن الآن ليس مجرد استدعاءات فقط، وإنما كل العمليات في الوسيط يُعاد توجيها إلى التوابع الأصلية. لذلك سيعيد الاستدعاء sayHi.length
النتيجة الصحيحة بعد التغليف في السطر (*)
.
وبذلك حصلنا على مُغلِّف أغنى بالمميزات من الطريقة السابقة.
هنالك العديد من الاعتراضات الأخرى: يمكنك العودة لبداية المقال لقراءة القائمة الكاملة للاعتراضات. كما أن طريقة استخدامها مشابه كثيرًا لما سبق.
الانعكاس
الانعكاس Reflect
هو عبارة عن كائن مضمّن في اللغة يبسط إنشاء الوسيط Proxy
.
ذكرنا سابقًا أن الدوالّ الداخلية، مثل: [[Get]]
و[[Set]]
وغيرها مخصصة للاستخدام في مواصفات اللغة فقط، ولا يمكننا استدعاؤها مباشرة.
يمكن لكائن المنعكس Reflect
من فعل ذلك إلى حد ما. إذ أن الدوالّ الخاصة به عبارة مُغلِّفات صغيرة حول الدوالّ الداخلية.
فيما يلي أمثلة للعمليات واستدعاءات المنعكس Reflect
الّتي ستُؤدي نفس المهمة:
الدالة الداخلية | الدالة المقابلة في المنعكس | العملية |
---|---|---|
[[Get]]
|
Reflect.get(obj, prop)
|
obj[prop]
|
[[Set]]
|
Reflect.set(obj, prop, value)
|
obj[prop] = value
|
[[Delete]]
|
Reflect.deleteProperty(obj, prop)
|
delete obj[prop]
|
[[Construct]]
|
Reflect.construct(F, value)
|
new F(value)
|
… | … | … |
فمثلًا:
let user = {}; Reflect.set(user, 'name', 'John'); alert(user.name); // John
تحديدًا، يتيح لنا المنعكس Reflect
استدعاء العمليات (new
, delete
…) كتوابع هكذا (Reflect.construct
,Reflect.deleteProperty
, …). وهذه الامكانيات مثيرة للاهتمام، ولكن هنالك شيء آخر مهم.
لكل دالّة داخلية، والّتي يمكننا تتبعها من خلال الوسيط Proxy
، يوجد دالّة مقابلة لها في المنعكس Reflect
، بنفس الاسم والوسطاء أي مشابه تمامًا للاعتراض في الوسيط Proxy
.
لذا يمكننا استخدام المنعكس 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
.
أي أن كل شيء بسيط: إذا كان الاعتراض يُعيد توجيه الاستدعاء إلى الكائن، فيكفي استدعاء Reflect.<method>
بنفس الوسطاء.
في معظم الحالات ، يمكننا فعل الشيء نفسه بدون Reflect
، على سبيل المثال، يمكن استبدال Reflect.get(target, prop, receiver)
بـ target[prop]
. يوجد فروقٍ بينهم ولكن لا تكاد تذكر.
استخدام الوسيط مع الجالِب
لنرى مثالاً يوضح لماذا 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
إن الاعتراض 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" }; // نتوقع ظهور الكلمة: Admin alert(admin.name); // النتيجة: Guest (لماذا؟)
إن قراءة admin.name
يجب أن تُعيد كلمة "Admin"
وليس "Guest"
!
ما الذي حدث؟ لعلنا أخطأنا بشيء ما في عملية الوراثة؟
ولكن إذا أزلنا كائن الوسيط، فسيكون كل شيء على ما يرام.
إذًا المشكلة الحقيقية في الوسيط، تحديدًا في السطر (*)
.
-
عندما نقرأ خاصّية الاسم
admin.name
من كائنadmin
، لا يحتوي كائنadmin
على هذه الخاصية، وبذلك ينتقل للبحث عنها في النموذج الأولي الخاص به. -
النموذج الأولي الخاص به هو
userProxy
. -
عند قراءة الخاصية
name
من الوسيط، سيُشغّل اعتراضget
الخاص به، وسيُعيدih من الكائن الأصلي هكذاtarget[prop]
في السطر(*)
.يؤدي الاستدعاء
target [prop]
، عندما يكون قيمةprop
هي الجالب (getter)، سيؤدي ذلك لتشغيل الشيفرة بالسياقِthis = target
. لذلك تكون النتيجةthis._name
من الكائن الأصلي للهدفtarget
، أي: منuser
.
لإصلاح مثل هذه المواقف، نحتاج إلى receiver
، الوسيط الثالث للاعتراض get
. إذ سيُحافظ على قيمة this
الصحيحة لتُمرر بعد ذلك إلى الجالِب (getter). في حالتنا تكون قيمتها هي 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
الصحيحة (وهي admin
)، والّتي ستُمرر من خلال Reflect.get
في السطر (*)
.
يمكننا إعادة كتابة الاعتراض بطريقة أقصر:
get(target, prop, receiver) { return Reflect.get(...arguments); }
استدعاءات المنعكس Reflect
لها نفس أسماء اعتراضات الوسيط وتقبل نفس وسطائه أيضًا. إذ صُمّمت خصيصًا لهذا الغرض.
لذا ، فإن استخدام المنعكس return Reflect...
يزودنا بالأمان والثقة لتوجيه العملية والتأكد تمامًا من أننا لن ننسَ أي شيء متعلقٌ بها.
قيود الوسيط
لدى الوسيط طريقة فريدة لتغيير أو تعديل سلوك الكائنات الموجودة عند أدنى مستوى. ومع ذلك، هذه الطريقة ليست مثالية. وإنما هناك قيود.
كائنات مضمّنة: فتحات داخلية
تستخدم العديد من الكائنات المضمنة، مثل الكائنات Map
وSet
وDate
وPromise
وغيرها ما يسمى بـ" الفتحات الداخلية ".
وهي مشابهة للخاصيات، لكنها محفوظة للأغراض داخلية فقط، وللمواصفات القياسية للغة فقط. فمثلًا تخزن Map
العناصر في الفتحة الداخلية [[MapData]]
. وتستطيع الدوال المُضمّنة الوصول إليها مباشرةً، وليس عبر الدوالّ الداخلية مثل [[Get]]/[[Set]]
. لذا فإن الوسيط Proxy
لا يمكنه اعتراض ذلك.
ولكن ما سبب اهتمامنا بذلك؟ إنها بكلّ الأحوال داخلية!
حسنًا ، إليك المشكلة. بعد أن يغلف الكائن المضمن مثل: Map باستخدام الوسيط، لن يمتلك الوسيط هذه الفتحات الداخلية، لذلك ستفشل الدوالّ المضمنة.
إليك مثالًا يوضح الأمر:
let map = new Map(); let proxy = new Proxy(map, {}); proxy.set('test', 1); // Error
داخليًا ، تخزن Map
جميع البيانات في الفتحة الداخلية [[MapData]]
. ولكن الوسيط ليس لديه مثل هذه الفتحة. تحاول الدالّة المضمنة Map.prototype.set
الوصول إلى الخاصية الداخلية this.[[MapData]]
، ولكن لأن this=proxy
، لا يمكن العثور عليه في الوسيط 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!)
الآن تعمل وفق المطلوب، لأن اعتراض get
يربط خاصيات الدوالّ، مثل map.set
، بالكائن الهدف (map
) نفسه.
بخلاف المثال السابق، فإن قيمة this
داخلproxy.set (...)
لن تكون proxy
، وإنما map
الأصلية. لذا عند التطبيق الداخلي لـ set
سيحاول الوصول إلى الفتحة الداخلية هكذا this.[[MapData]]
، ولحسن الحظ سينجح.
ملاحظة: المصفوفة العادية Array
ليس لها فتحات داخلية
استثناء ملحوظ: لا تستخدم المصفوفات المضمنة Array
الفتحات الداخلية. هذا لأسباب تاريخية، إذ إنها ظهرت منذ وقت طويل.
لذلك لا توجد مشكلة عند تغليف المصفوفة إلى باستخدام الوسيط.
الخاصيات الخاصة
يحدث شيء مشابه للأمر مع خاصيات الصنف الخاصة.
فمثلًا، يمكن للدالّة getName()
الوصول إلى الخاصية الخاصًة #name
بدون استخدام الوسيط، ولكن بعد تغليفنا للكائن باستخدام الوسيط ستتوقف إمكانية وصول الدالّة السابقة للخاصّية الخاصّة:
class User { #name = "Guest"; getName() { return this.#name; } } let user = new User(); user = new Proxy(user, {}); alert(user.getName()); // Error
وذلك بسبب أن التنفيذ الفعلي للخاصّيات الخاصّة يكون باستخدام الفتحات داخلية. ولا تستخدم لغة جافاسكربت الدوالّ [[Get]]/[[Set]]
للوصول إليها.
في استدعاء getName()
، تكون قيمة this
هي كائن user
المغلّف بالوسيط، ولا يحتوي -هذا الكائن- على فتحة داخلية مع هذه الخاصّيات الخاصّة.
وللمرة الثانية، يكون ربط الدالّة بالكائن من سيحل الأمر:
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
ومع ذلك، فإن لهذا الحل بعض العيوب، كما وضحنا سابقًا: سيعرض هذا الحل الكائن الأصلي لبعض الدوالّ، مما سيُسمح بتمريره لدوالٍ أكثر وبذلك كسر الدوالّ الأخرى المتعلقة بالوسيط.
Proxy! = target
إن كلًا من الوسيط والكائن الأصلي مختلفان. وهذا أمر طبيعي، أليس كذلك؟
لذلك إذا استخدمنا الكائن الأصلي كخاصية في المجموعة Set
، ثم غلفناه بالوسيط، فعندئذ لن نتمكن من العثور على الوسيط:
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
، لأن الوسيط هو كائن مختلف عن الكائن الأصلي.
لا يمكن للوسطاء proxies
اعتراض اختبار المساواة الصارم ===
.
يمكنها اعترلض العديد من العمليات الأخرى، مثل: new
(مع build
) وin
(مع has
) وdelete
(مع deleteProperty
) وما إلى ذلك.
ولكن لا توجد طريقة لاعتراض اختبار المساواة الصارم للكائنات. الكائن يساوي نفسه تمامًا ولا يساوي أيّ كائنٍ آخر.
لذا فإن جميع العمليات والأصناف المضمّنة في اللغة التي توازن بين الكائنات من أجل المساواة ستميز الفرق بين الكائن الأصلي والوسيط. ولا يوجد من يحل محله لإصلاح الأمر.
الوسيط القابل للتعطيل
وسيط revocable هو وسيط يمكن تعطيله.
لنفترض أن لدينا موردًا ونود منع الوصول إليه في لحظةٍ ما.
أحد الأشياء التي يمكننا فعلها هو تغليف هذا المورد بالوسيط القابل للتعطيل، بدون أي اعتراضات. بهذه الحالة سيُعيد الوسيط توجيه العمليات للكائن، ويمكننا تعطيله بأي لحظة نريدها.
وصياغته:
let {proxy, revoke} = Proxy.revocable(target, handler)
الاستدعاء من خلال proxy
سيُعيد الكائن والاستدعاء من خلال revoke
سيعطل إمكانية الوصول إليه.
إليك المثال لتوضيح الأمر:
let object = { data: "Valuable data" }; let {proxy, revoke} = Proxy.revocable(object, {}); // مرر الوسيط لمكان آخر بدل الكائن alert(proxy.data); // Valuable data // لاحقا نستدعي التابع revoke(); // لن يعمل الوسيط الآن (لأنه معطلّ) alert(proxy.data); // Error
يؤدي استدعاء revoke()
لإزالة جميع المراجع الداخلية للكائن الهدف من الوسيط، وبذلك لم يعد متصل بأي شيء بعد الآن. كما يمكننا بعد ذلك كنس المخلفات من الذاكرة بإزالة الكائن الهدف.
يمكننا أيضًا تخزين revoke
في WeakMap
، حتى نتمكن من العثور عليه بسهولة من خلال كائن الوسيط:
let revokes = new WeakMap(); let object = { data: "Valuable data" }; let {proxy, revoke} = Proxy.revocable(object, {}); revokes.set(proxy, revoke); // ..لاحقًا في الشيفرة.. revoke = revokes.get(proxy); revoke(); alert(proxy.data); // Error (revoked)
تفيدنا هذه الطريقة بأنه ليس علينا بعد الآن حمل revoke
. وإنما يمكننا الحصول عليها من map
من خلال الوسيط proxy
عند الحاجة.
نستخدم WeakMap
بدلاً من Map
هنا لأنه لن يمنع كنس المخلّفات في الذاكرة. إذا أصبح كائن الوسيط "غير قابل للوصول" (مثلًا، في حال لم يعد هناك متغيّر يشير إليه بعد الآن)، فإن WeakMap
يسمح بمسحه من الذاكرة مع revoke
خاصته والّتي لن نحتاج إليها بعد الآن.
المصادر
- المواصفات القياسية للوسيط : Proxy.
- توثيق الوسيط الرسمي من مركز مطوري موزيلا MDN.
خلاصة
الوسيط Proxy
عبارة عن غلاف حول كائن، يُعيد توجيه العمليات عليه إلى الكائن، ويحبس بعضها بشكل اختياري.
يمكنه تغليف أي نوع من الكائنات، بما في ذلك الأصناف والدوالّ.
صياغته:
let proxy = new Proxy(target, { /* traps */ });
… ثم يجب علينا استخدام "الوسيط" في كل مكان بدلاً من كائن "الهدف". لا يمتلك الوكيل خاصيات أو توابع. يعترض عملية ما إذا زُودَ بالاعتراض المناسب، وإلا سيعيد توجيهها إلى كائن الهدف target
.
يمكننا اعتراض:
-
قراءة (
get
) وكتابة (set
) وحذف (deleteProperty
) خاصية (حتى الخاصية غير موجودة). -
استدعاء دالّة ما (الاعتراض
apply
). -
المعامل
new
(الاعتراضconstruct
). - العديد من العمليات الأخرى (القائمة الكاملة في بداية المقال وفي التوثيق الرسمي).
مما سيسمح لنا بإنشاء خاصيات ودوالّ "افتراضية"، وتطبيق قيم افتراضية، وكائنات المراقبة، وزخرفة الدوالّ، وأكثر من ذلك بكثير.
يمكننا أيضًا تغليف كائن ما عدة مرات في وسطاء مختلفة، وزخرفته بمختلف أنواع الوظائف. صُممّت الواجهة البرمجية للمنعكس لتكمل عمل الوسيط. بالنسبة لأي اعتراض Proxy
، هناك استدعاء للمنعكس Reflect
مقابل له بنفس الوسطاء. يجب علينا استخدامها لإعادة توجيه الاستدعاءات إلى الكائنات المستهدفة.
لدى الوسيط بعض القيود:
- تحتوي الكائنات المضمّنة في اللغة على "فتحات داخلية"، ولا يمكن الوصول إلى تلك الأشياء بالوسيط. راجع الفقرة المخصصة لها أعلاه.
-
وينطبق الشيء نفسه على خاصيات الصنف الخاصة، إذ تنفيذها داخليًا باستخدام الفتحات. لذا يجب أن تحتوي استدعاءات دوالّ الوسيط على الكائن المستهدف بدل
this
للوصول إليها. -
لا يمكن اعتراض اختبارات المساواة الصارمة للكائن
===
. - الأداء: تعتمد المقاييس على المحرك، ولكن عمومًا إن الوصول إلى الخاصية باستخدام وكيل بسيط سيستغرق وقتًا أطول بعض الشيء. عمليًا يهتم بها البعض لعدم حدوث اختناق في الأداء "عنق الزجاجة".
تمارين
خطأ في قراءة الخاصيات غير موجودة في الكائن الأصلي
عادةً ، تؤدي محاولة قراءة خاصية غير موجودة إلى إعادة النتيجة undefined
.
أنشئ وسيطًا يعيد خطأ عند محاولة قراءة خاصية غير موجودة في الكائن الأصلي بدلًا من ذلك.
يمكن أن يساعد ذلك في الكشف عن الأخطاء البرمجية مبكرًا.
اكتب دالّة wrap(target)
والّتي تأخذ كائنًا target
وتعيد وسيطًا والّذي سيُضيف خصائص وظيفية أخرى.
هكذا يجب أن تعمل:
let user = { name: "John" }; function wrap(target) { return new Proxy(target, { /* your code */ }); } user = wrap(user); alert(user.name); // John alert(user.age); // ReferenceError: Property doesn't exist "age"
الحل
let user = { name: "John" }; function wrap(target) { return new Proxy(target, { get(target, prop, receiver) { if (prop in target) { return Reflect.get(target, prop, receiver); } else { throw new ReferenceError(`Property doesn't exist: "${prop}"`) } } }); } user = wrap(user); alert(user.name); // John alert(user.age); // ReferenceError: Property doesn't exist "age"
الوصول إلى الدليل [-1] في فهرس المصفوفة
يمكننا الوصول إلى عناصر المصفوفة باستخدام الفهارس السلبية في بعض لغات البرمجة، محسوبةً بذلك من نهاية المصفوفة.
هكذا:
let array = [1, 2, 3]; array[-1]; // 3, آخر عنصر في المصفوفة array[-2]; // 2, خطوة للوراء من نهاية المصفوفة array[-3]; // 1, خطوتين للوراء من نهاية المصفوفة
بتعبيرٍ آخر ، فإن array[-N]
هي نفس array[array.length - N]
.
أنشئ وسيطًا لتنفيذ هذا السلوك.
هكذا يجب أن تعمل:
let array = [1, 2, 3]; array = new Proxy(array, { /* your code */ }); alert( array[-1] ); // 3 alert( array[-2] ); // 2 // بقية الخصائص الوظيفية الأخرى يجب أن تبقى كما هي
الحل
let array = [1, 2, 3]; array = new Proxy(array, { get(target, prop, receiver) { if (prop < 0) { // حتى وإن وصلنا للمصفوفة هكذا arr[1] // إن المتغيّر prop عبارة عن سلسلة نصية لذا نحتاج لتحويله إلى رقم prop = +prop + target.length; } return Reflect.get(target, prop, receiver); } }); alert(array[-1]); // 3 alert(array[-2]); // 2
المراقب
أنشئ تابع makeObservable(target)
الّذي تجعل الكائن قابلاً للمراقبة 'من خلال إعادة وسيط.
إليك كيفية العمل:
function makeObservable(target) { /* your code */ } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); user.name = "John"; // alerts: SET name=John
وبعبارة أخرى، فإن الكائن الّذي سيعاد من خلال makeObservable
يشبه تمامًا الكائن الأصلي، ولكنه يحتوي أيضًا على الطريقة observe(handler)
الّتي ستضبط تابع المُعالج
ليستدعى عند أي تغيير في الخاصية.
عندما تتغير خاصية ما، يستدعى handler(key, value)
مع اسم وقيمة الخاصية.
ملاحظة. في هذا التمرين، يرجى الاهتمام بضبط الخاصيات فقط. يمكن تنفيذ عمليات أخرى بطريقة مماثلة.
الحل
يتكون الحل من جزئين:
-
عندما يستدعى
.observe(handler)
، نحتاج إلى حفظ المعالج في مكان ما، حتى نتمكن من الاتصال به لاحقًا. يمكننا تخزين المعالجات في الكائن مباشرة، باستخدام الرمز الخاص بنا كمفتاح خاصية. -
سنحتاج لوسيط مع الاعتراض
set
لاستدعاء المعالجات عند حدوث أي تغيير.
let handlers = Symbol('handlers'); function makeObservable(target) { // 1. هيئ مخزّن المعالجات target[handlers] = []; // احتفظ بتوابع المعالج في مصفوفة للاستدعاءات اللاحقة target.observe = function(handler) { this[handlers].push(handler); }; // 2. أنشئ وسيط ليعالج التغييرات return new Proxy(target, { set(target, property, value, receiver) { let success = Reflect.set(...arguments); // وجه العملية إلى الكائن if (success) { // إن حدث خطأ ما في ضبط الخاصية // استدعي جميع المعالجات target[handlers].forEach(handler => handler(property, value)); } return success; } }); } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); user.name = "John";
ترجمة -وبتصرف- للفصل Proxy and Reflect من كتاب The JavaScript language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.