يوجد نوعين من الخاصيات. الأوّل هو خاصيات البيانات (Data Properties). نعرف جيدًا كيف نعمل مع هذا النوع إذ كلّ ما استعملناه من البداية إلى حدّ الساعة هي خاصيات بيانات.
النوع الثاني هو الجديد، وهو خاصيات الوصول (Accessor Properties). هي في الأساس دوال تجلب القيم وتضبطها، ولكن في الشيفرة تظهرُ لنا وكأنها خاصيات عادية.
الجالبات والضابطات
خاصيات الوصول هذه هي توابِع ”جلب“ (getter) و”ضبط“ (setter).
let obj = { get propName() { // جالب، يُستعمَل جلب قيمة الخاصية obj.propName }, set propName(value) { // ضابط يُستعمَل لضبط قيمة الخاصية obj.propName إلى value } };
يعمل الجالب متى ما طلبت قراءة الخاصية obj.propName
، والضابط… متى ما أردت إسناد قيمة obj.propName = value
.
لاحظ مثلًا كائن user
له خاصيتين: اسم name
ولقب surname
:
let user = { name: "John", surname: "Smith" };
الآن نريد إضافة خاصية الاسم الكامل fullName
، وهي "John Smith"
. طبعًا لا نريد نسخ المعلومات ولصقها، لذا سنُنفذها باستخدام خاصية الوصول (ِget):
let user = { name: "John", surname: "Smith", // لاحظ get fullName() { return `${this.name} ${this.surname}`; } }; alert(user.fullName); // John Smith
خارج الكائن لا تبدو خاصية الوصول إلا خاصية عادية، وهذا بالضبط الغرض من هذه الخاصيات، فلسنا نريد استدعاء user.fullName
على أنّها دالة، بل قراءتها فحسب، ونترك الجالب يقوم بعمله خلف الكواليس.
الآن ليس للخاصية fullName
إلا جالبًا، لو حاولنا إسناد قيمة لها user.fullName=
فسنرى خطأً:
let user = { get fullName() { return `...`; } }; user.fullName = "Test"; // خطأ (للخاصية جالب فقط)
هيًا نُصلح الخطأ ونُضيف ضابطًا للخاصية user.fullName
:
let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; }, // هنا set fullName(value) { [this.name, this.surname] = value.split(" "); } }; // نضبط fullName كما النية بتمرير القيمة. user.fullName = "Alice Cooper"; alert(user.name); // Alice alert(user.surname); // Cooper
وهكذا صار لدينا الخاصية ”الوهمية“ fullName
. يمكننا قراءتها والكتابة عليها، ولكنها في واقع الأمر، غير موجودة.
واصفات الوصول (Accessor Descriptors)
واصِفات خاصيات الوصول (Accessor Properties) تختلف عن واصِفات خاصيات البيانات (Data Properties). فليس لخاصيات الوصول قيمة value
أو راية writable
، بل هناك دالة get
ودالة set
.
أي يمكن لواصِف الوصول أن يملك مايلي:
-
get
-- دالة ليس لها وُسطاء تعمل متى ما قُرئت الخاصية. -
set
-- دالة لها وسيط واحد تُستدعى متى ما ضُبطت الخاصية. -
enumerable
-- خاصية قابلية الإحصاء وهي مشابهة لخاصيّات البيانات. -
configurable
-- خاصية قابلية إعادة الضبط وهي مشابهة لخاصيّات البيانات.
فمثلًا لننشئ خاصية الوصول fullName
باستعمال التابِع defineProperty
، يمكننا تمرير واصِفًا فيه دالة get
ودالة set
:
let user = { name: "John", surname: "Smith" }; // هنا Object.defineProperty(user, 'fullName', { get() { return `${this.name} ${this.surname}`; }, set(value) { [this.name, this.surname] = value.split(" "); } }); alert(user.fullName); // John Smith for(let key in user) alert(key); // name, surname
أُعيد بأنّ الخاصية إمّا تكون خاصية وصول (لها توابِع get/set
) أو خاصية بيانات (لها قيمة value
)، ولا تكون الاثنين معًا. فلو حاولنا تقديم get
مع value
في نفس الواصِف، فسنرى خطأً:
// خطأ: واصِف الخاصية غير صالح Object.defineProperty({}, 'prop', { get() { return 1 }, value: 2 });
الجوالب والضوابط الذكية
يمكننا استعمال الجوالب والضوابط كأغلفة للخاصيات ”الفعلية“، فتكون في يدنا السيطرة الكاملة على العمليات التي تؤثّر عليها. فمثلًا لو أردنا منع الأسماء القصيرة للاسم user
فيمكن كتابة الضابِط name
وترك القيمة في خاصية منفصلة باسم _name
:
let user = { get name() { return this._name; }, set name(value) { if (value.length < 4) { alert("Name is too short, need at least 4 characters"); // الاسم قصير جدًا. أقلّ طول هو 4 محارف return; } this._name = value; } }; user.name = "Pete"; alert(user.name); // Pete user.name = ""; // الاسم قصير جدًا...
هكذا نخزّن الاسم في الخاصية _name
والوصول يكون عبر الجالب والضابط.
عمليًا يمكن للشيفرة الخارجية الوصول إلى الاسم مباشرةً باستعمال user._name
، ولكن هناك مفهوم شائع بين المطوّرين هو أنّ الخاصيات التي تبدأ بشرطة سفلية "_"
هي خاصيات داخلية وممنوع التعديل عليها من خارج الكائن.
استعمالها لغرض التوافقية
إحدى استعمالات خاصيات الوصول هذه هي إتاحة الفرصة للتحكّم بخاصية بيانات ”عادية“ متى أردنا واستبدالها بدالتي جلب وضبط وتعديل سلوكها.
لنقل مثلًا بأنّا بدأنا المشروع حيث كانت كائنات المستخدمين تستعمل خاصيات البيانات: الاسم name
والعمر age
:
function User(name, age) { this.name = name; this.age = age; } let john = new User("John", 25); alert( john.age ); // 25
ولكن الأمور لن تبقى على حالها وإنما ستتغير، عاجلًا أم آجلًا. فبدل العمر age
نقول بأنّا نريد تخزين تاريخ الميلاد birthday
إذ هو أكثر دقّة وسهولة في الاستعمال:
function User(name, birthday) { this.name = name; this.birthday = birthday; } let john = new User("John", new Date(1992, 6, 1));
ولكن… كيف سنتعامل مع الشيفرة القديمة الّتي مازالت تستعمل خاصية age
؟
يمكن أن نبحث في كلّ أمكان استخدام الخاصية age
وتغييرها بخاصية جديدة مناسبة، ولكنّ ذلك يأخذ وقتًا ويمكن أن يكون صعبًا لو عدة مبرمجين يعملون على هذه الشيفرة، كما وأنّ وجود عمر المستخدم كخاصية age
أمر جيّد، أليس كذلك؟
إذًا لنُبقي الخاصية كما هي، ونُضيف جالبًا للخاصية تحلّ لنا المشكلة:
function User(name, birthday) { this.name = name; this.birthday = birthday; // العمر هو الفرق بين التاريخ اليوم وتاريخ الميلاد Object.defineProperty(this, "age", { get() { let todayYear = new Date().getFullYear(); return todayYear - this.birthday.getFullYear(); } }); } let john = new User("John", new Date(1992, 6, 1)); alert( john.birthday ); // تاريخ الميلاد موجود alert( john.age ); // ...وعمر المستخدم أيضًا
هكذا بقيت الشيفرة القديمة تعمل كما نريد، وأضفنا الخاصية الإضافية.
ترجمة -وبتصرف- للفصل Property getters and setters من كتاب The JavaScript language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.