دليل تعلم جافاسكربت رايات الخاصيات وواصفاتها في جافاسكربت


صفا الفليج

كما نعلم فالكائنات تُخزّن داخلها خاصيات (properties) تصفها. وحتى الآن لم تكن الخاصية إلا زوجًا من مفاتيح وقيم، ولكن خاصية الكائن يمكن أن تكون أكثر قوّة ومرونة من هذا.

سنتعلم في هذا المقال بعض خصائص الضبط الأخرى، وفي الفصل الّذي يليه سنرى كيف نحوّلها إلى دوال جلب/ضبط (Setters/Getters) أيضًا.

رايات الخاصيات

لخصائص الكائنات (إضافةً إلى القيمة الفعلية لها) ثلاث سمات أخرى مميزة (أو ”رايات“ flags):

  • قابلية التعديل -- لو كانت بقيمة true فيمكننا تغيير القيمة وتعديلها، ولو لم تكن فالقيمة للقراءة فقط.
  • قابلية الإحصاء -- لو كانت بقيمة true، فستقدر الحلقات على المرور على عناصرها، وإلا فلن تقدر.
  • قابلية إعادة الضبط -- لو كانت بقيمة true فيمكن حذف الخاصية وتعديل هذه السمات، وإلا فلا يمكن.

لم نتطرّق إلى هذه الرايات قبلًا إذ لا تظهر عادةً في الشيفرات، فحين ننشئ خاصية "بالطريقة العادية" فكلّ هذه السمات بقيمة true، ولكن يمكننا طبعًا تغييرها متى أردنا.

أولًا لنعرف كيف سنرى هذه الرايات.

يتيح لنا التابِع Object.getOwnPropertyDescriptor الاستعلامَ عن المعلومات الكاملة الخاصة بأيّ خاصية.

وهذه صياغته:

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
  • obj: الكائن الّذي سنجلب معلوماته.

  • propertyName: اسم الخاصية الّتي نريد.

نسمّي القيمة المُعادة بكائن ”واصِف الخاصيات“ (Property Descriptor)، وهو يحتوي على القيمة وجميع الرايات الّتي سبق لنا شرحها.

إليك مثالًا:

let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/* واصف الخاصية:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/

يمكننا استعمال التابِع Object.defineProperty لتغيير الرايات.

إليك صياغته:

Object.defineProperty(obj, propertyName, descriptor)
  • obj و propertyName: الكائن الّذي سنطبّق عليه الواصِف، واسم الخاصية.

  • descriptor: واصِف الخاصيات الّذي سنطبّقه على الكائن.

لو كانت الخاصية موجودة فسيُحدّث التابع defineProperty راياتها. وإلّا فسيُنشئ الخاصية بهذه القيمة الممرّرة والرايات كذلك، وفي هذه الحالة لو لم يجد قيمة لأحد الرايات، فسيعدّه بقيمة false.

مثلًا هنا نُنشئ الخاصية name حيث تكون راياتها كلّها بقيمة false:

let user = {};

Object.defineProperty(user, "name", {
  value: "John"
});

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": "John",
  // لاحظ هنا
  "writable": false,
  "enumerable": false,
  "configurable": false
}
 */

وازِن هذه الخاصية بتلك الّتي صنعناها أعلاه user.name (كالعادة): وأصبحت قيمة جميع الرايات false. لو لم يكن هذا ما تريده فربّما الأفضل ضبطها على true في كائن descriptor.

لنرى الآن تأثيرات هذه الرايات في هذا المثال.

منع قابلية التعديل

لنمنع قابلية التعديل على الخاصية user.name (أي استحالة إسناد قيمة لها) وذلك بتغيير راية writable:

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  writable: false // هنا
});

user.name = "Pete"; 
// خطأ: لا يمكن إسناد القيم إلى الخاصية ‫ `name` إذ هي للقراءة فقط

الآن يستحيل على أيّ شخص تعديل اسم هذا المستخدم إلّا لو طبّقوا تابِع defineProperty من طرفهم ليُلغي ما فعلناه نحن.

لا تظهر الأخطاء إلّا في الوضع الصارم
لا تظهر أي أخطاء عند تعديل قيمة خاصية في وضع غير الصارم إلّا أنها لن تتغير قيمتها بطبيعة الحال، وذلك لأن خطأ خرق الراية لن يظهر إلا في الوضع الصارم.

إليك نفس المثال ولكن دون إنشاء الخاصية من الصفر:

let user = { };

Object.defineProperty(user, "name", {
  value: "John",
  // لو كانت الخاصيات جديدة فعلينا إسناد قيمها إسنادًا صريحًا
  enumerable: true,
  configurable: true
});

alert(user.name); // John
user.name = "Pete"; // Error

منع قابلية الإحصاء

الآن لنُضيف تابِع toString مخصّص على كائن user.

عادةً لا يمكننا استخدام التابع toString على الكائنات، وذلك لأنها غير قابلة للإحصاء، ولذلك فلا يمكن تمريرها على حلقة for..in. ولكن إن أردنا تغيير ذلك يدويًا (كما في المثال التالي) عندها يمكننا تمريرها إلى حلقة for..in.

let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

// مبدئيًا، ستعرض الشيفرة الخاصيتين معًا:
for (let key in user) alert(key); // name, toString

لو لم نرد ذلك فيمكن ضبط enumerable:false حينها لن نستطع أن نمرر الكائن في حلقات for..in كما في السلوك المبدئي:

let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

Object.defineProperty(user, "toString", {
  enumerable: false // هنا
});

// الآن اختفى تابِع toString:
for (let key in user) alert(key); // name

كما أنّ التابِع Object.keys يستثني الخاصيات غير القابلة للإحصاء:

alert(Object.keys(user)); // name

منع قابلية إعادة الضبط

أحيانًا ما نرى راية ”قابلية إعادة الضبط“ ممنوعة (أي configurable:false) في بعض الكائنات والخاصيات المضمّنة في اللغة. لا يمكن حذف هذه الخاصية لو كانت ممنوعة (أي configurable:false).

فمثلًا المتغيّر المضمّن في اللغة Math.PI يمنع قابلية التعديل والإحصاء وإعادة الضبط عليه:

let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": 3.141592653589793,
  "writable": false,
  "enumerable": false,
  "configurable": false
}
*/

هكذا لا يقدر المبرمج على تغيير قيمة Math.PI ولا الكتابة عليها.

Math.PI = 3; // خطأ

// delete Math.PI لن تعمل أيضًا

إن تفعيل خاصيّة منع قابلية إعادة الضبط هو قرار لا عودة فيه، فلا يمكننا تغيير الراية (إتاحة قابلية إعادة الضبط) باستعمال التابِع defineProperty.

وللدقّة فهذا المنع يضع تقييدات أخرى على defineProperty:

  1. منع تغيير راية قابلية إعادة الضبط configurable.
  2. منع تغيير راية قابلية الإحصاء enumerable.
  3. منع تغيير راية قابلية التعديل writable: false إلى القيمة true (ولكن العكس ممكن).
  4. منع تغيير ضابط وجالب واصف الوصول get/set (ولكن يمكن إسناد قيم إليه).

هنا سنحدّد الخاصية user.name لتكون ثابتة للأبد :

let user = { };

Object.defineProperty(user, "name", {
  value: "John",
  writable: false,
  configurable: false
});


Object.defineProperty(user, "name", {writable: true}); // خطأ

نلاحظ عدم إمكانية تغيير الخاصيّة user.name ولا حتى راياتها ولن نستطيع تطبيق هذه العمليات عليها:

  1. الكتابة عليها user.name = "Pete"‎.
  2. حذفها delete user.name.
  3. تغيير قيمتها باستخدام التابع defineProperty هكذا: defineProperty(user, "name", { value: "Pete" })‎.

”إن منع قابلية إعادة الضبط“ ليس ”منعًا لقابلية التعديل“ إن فكرة منع قابلية إعادة الضبط هي في الحقيقة لمنع تغيير رايات هذه الخاصية أو حذفها، وليس تغيير قيمة الخاصية بحد ذاتها.

ملاحظة: في المثال السابق جعلنا قابلية التعديل ممنوعة يدويًا.

التابع Object.defineProperties

هناك أيضًا التابِع Object.defineProperties إذ يُتيح تعريف أكثر من خاصية في وقت واحد.

صياغته هي:

Object.defineProperties(obj, {
  prop1: descriptor1,
  prop2: descriptor2
  // ...
});

مثال عليه:

Object.defineProperties(user, {
  name: { value: "John", writable: false },
  surname: { value: "Smith", writable: false },
  // ...
});

أي أنّنا نقدر على ضبط أكثر من خاصية معًا.

التابع Object.getOwnPropertyDescriptors

يمكننا استعمال التابِع Object.getOwnPropertyDescriptors(obj)‎ لجلب كلّ واصفات الخاصيات معًا. ويمكن استعماله بدمجه مع Object.defineProperties لنسخ الكائنات ”ونحن على علمٍ براياتها“:

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

فعادةً حين ننسخ كائنًا نستعمل الإسناد لنسخ الخاصيات، هكذا:

for (let key in user) {
  clone[key] = user[key]
}

ولكن… هذا لا ينسخ معه الرايات. لذا يفضّل استعمال Object.defineProperties لو أردنا نُسخةً ”أفضل“ عن الكائن.

الفرق الثاني هو أنّ حلقة for..in تتجاهل الخاصيات الرمزية (Symbolic Properties)، ولكنّ التابِع Object.getOwnPropertyDescriptors يُعيد كلّ واصِفات الخاصيات بما فيها الرمزية.

إغلاق الكائنات على المستوى العام

تعمل واصِفات الخاصيات على مستوى الخاصيات منفردةً. هناك أيضًا توابِع تقصر الوصول إلى الكائن كلّه:

Object.preventExtensions(obj)

يمنع إضافة خاصيات جديدة إلى الكائن.

Object.seal(obj)

يمنع إضافة الخاصيات وإزالتها، فهو يمنع قابلية إعادة الضبط configurable: false على كلّ الخاصيات الموجودة.

Object.freeze(obj)

يمنع إضافة الخاصيات أو إزالتها أو تغييرها، إذ يمنع قابلية التعديل writable: false وقابلية إعادة الضبط configurable: false على كلّ الخاصيات الموجودة.

كما أنّ هناك توابِع أخرى تفحص تلك المزايا:

Object.isExtensible(obj)

يُعيد false لو كان ممنوعًا إضافة الخاصيات، وإلا true.

Object.isSealed(obj)

يُعيد true لو كان ممنوعًا إضافة الخاصيات أو إزالتها، وكانت كلّ خاصيات الكائن الموجودة ممنوعة من قابلية إعادة الضبط configurable: false.

Object.isFrozen(obj)

يُعيد true لو كان ممنوعًا إضافة الخاصيات أو إزالتها أو تغييرها، وكانت كلّ خاصيات الكائن الموجودة ممنوعة أيضًا من قابلية التعديل writable: false أو إعادة الضبط configurable: false.

أمّا على أرض الواقع، فنادرًا ما نستعمل هذه التوابِع.

ترجمة -وبتصرف- للفصل Property flags and descriptors من كتاب The JavaScript language





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن