دليل تعلم جافاسكربت جالبات الخاصيات وضابطاتها (Getters and Setters) في جافاسكربت


صفا الفليج

يوجد نوعين من الخاصيات. الأوّل هو خاصيات البيانات (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





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


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



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

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

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


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

تسجيل الدخول

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


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