كما نعلم فالكائنات تُخزّن داخلها خاصيات (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:
-
منع تغيير راية قابلية إعادة الضبط
configurable. -
منع تغيير راية قابلية الإحصاء
enumerable. -
منع تغيير راية قابلية التعديل
writable: falseإلى القيمةtrue(ولكن العكس ممكن). -
منع تغيير ضابط وجالب واصف الوصول
get/set(ولكن يمكن إسناد قيم إليه).
هنا سنحدّد الخاصية user.name لتكون ثابتة للأبد :
let user = { }; Object.defineProperty(user, "name", { value: "John", writable: false, configurable: false }); Object.defineProperty(user, "name", {writable: true}); // خطأ
نلاحظ عدم إمكانية تغيير الخاصيّة user.name ولا حتى راياتها ولن نستطيع تطبيق هذه العمليات عليها:
-
الكتابة عليها
user.name = "Pete". -
حذفها
delete user.name. -
تغيير قيمتها باستخدام التابع
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)
يمنع إضافة خاصيات جديدة إلى الكائن.
يمنع إضافة الخاصيات وإزالتها، فهو يمنع قابلية إعادة الضبط configurable: false على كلّ الخاصيات الموجودة.
يمنع إضافة الخاصيات أو إزالتها أو تغييرها، إذ يمنع قابلية التعديل writable: false وقابلية إعادة الضبط configurable: false على كلّ الخاصيات الموجودة.
كما أنّ هناك توابِع أخرى تفحص تلك المزايا:
يُعيد false لو كان ممنوعًا إضافة الخاصيات، وإلا true.
يُعيد true لو كان ممنوعًا إضافة الخاصيات أو إزالتها، وكانت كلّ خاصيات الكائن الموجودة ممنوعة من قابلية إعادة الضبط configurable: false.
يُعيد true لو كان ممنوعًا إضافة الخاصيات أو إزالتها أو تغييرها، وكانت كلّ خاصيات الكائن الموجودة ممنوعة أيضًا من قابلية التعديل writable: false أو إعادة الضبط configurable: false.
أمّا على أرض الواقع، فنادرًا ما نستعمل هذه التوابِع.
ترجمة -وبتصرف- للفصل Property flags and descriptors من كتاب The JavaScript language

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