اذهب إلى المحتوى

الوسيط Proxy والمنعكس Reflect في جافاسكربت


Mohamed Lahlah

يغلّف كائن الوكيل 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.

  1. تحدد عملية الكتابة proxy.test=‎ القيمة عند الهدف target، لاحظ السطر (1).
  2. تعيد عملية القراءة proxy.test القيمة من عند الهدف target. لاحظ السطر (2).
  3. تعيد عملية التكرار على الوسيط proxy القيم من الهدف target. لاحظ السطر (3).

كما نرى، الوسيط proxy في هذه الحالة عبارة عن غِلاف شفاف حول الكائن الهدف target.

proxy.png

لنُضف بعض الاعتراضات من أجل تفعيل المزيد من الإمكانيات.

ما الذي يمكننا اعتراضه؟

بالنسبة لمعظم العمليات على الكائنات، هناك ما يسمى "الدوالّ الداخلية" في مواصفات القياسية في اللغة، والّتي تصف كيفية عمل الكائن عند أدنى مستوى. فمثلًا الدالة الداخلية [[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.getOwnPropertyDescriptor
for..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"!

ما الذي حدث؟ لعلنا أخطأنا بشيء ما في عملية الوراثة؟

ولكن إذا أزلنا كائن الوسيط، فسيكون كل شيء على ما يرام.

إذًا المشكلة الحقيقية في الوسيط، تحديدًا في السطر (*).

  1. عندما نقرأ خاصّية الاسم admin.name من كائن admin، لا يحتوي كائن admin على هذه الخاصية، وبذلك ينتقل للبحث عنها في النموذج الأولي الخاص به.

  2. النموذج الأولي الخاص به هو userProxy.

  3. عند قراءة الخاصية 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 عبارة عن غلاف حول كائن، يُعيد توجيه العمليات عليه إلى الكائن، ويحبس بعضها بشكل اختياري.

يمكنه تغليف أي نوع من الكائنات، بما في ذلك الأصناف والدوالّ.

صياغته:

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)‎ مع اسم وقيمة الخاصية.

ملاحظة. في هذا التمرين، يرجى الاهتمام بضبط الخاصيات فقط. يمكن تنفيذ عمليات أخرى بطريقة مماثلة.

الحل

يتكون الحل من جزئين:

  1. عندما يستدعى ‎.observe(handler)‎، نحتاج إلى حفظ المعالج في مكان ما، حتى نتمكن من الاتصال به لاحقًا. يمكننا تخزين المعالجات في الكائن مباشرة، باستخدام الرمز الخاص بنا كمفتاح خاصية.
  2. سنحتاج لوسيط مع الاعتراض 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


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...