كما نعلم فالكائنات تُخزّن داخلها خاصيات (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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.