دليل تعلم جافاسكربت صياغة الأصناف الأساسية (Class basic syntax) في جافاسكربت


صفا الفليج
اقتباس

في البرمجة كائنية التوجّه، يعدّ الصنف عبارة عن توسعة للشيفرة البرمجية للبرنامج وذلك لإنشاء كائنات كما أنه يهيئ القيم الأولية لمتغيرات الحالة (أو تدعى أحيانًا خاصيات وهي المتغيرات الّتي تكون عضوًا بالصنف) وتطبيق سلوك معين (باستخدام الدوالّ أو التوابِع الأعضاء) - ويكيبيديا

بعيدًا عن الكلام النظري، ففي الواقع نريد عادةً إنشاء أكثر من كائن تحمل نفس النوع، مثل المستخدمين والبضائع وغيرها.

كما علمنا في الفصل "الباني والعامل "new" في جافاسكربت"، يمكن للتعليمة new function القيام بهذه المهمة.

ولكن، في لغة جافاسكربت الحديثة هناك بوانيٍ أكثر تقدّمًا للأصناف إذ تُتيح لنا مزايا جديدة مميّزة تفيدنا حين نُبرمج على طريقة البرمجة كائنية التوجه.

صياغة الأصناف class

إليك الصياغة الأساسية:

class MyClass {
  // توابِع الصنف
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

بعدها استعمل new MyClass()‎ لتُنشِئ كائنًا جديدًا فيه التوابِع تلك.

يُنادي الباني constructor()‎ العبارة new تلقائيًا ليهيئ الكائن في المكان المطلوب.

إليك مثالًا:

class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }

}

// الاستعمال:
let user = new User("John");
user.sayHi();

متى استدعينا new User("John")‎:

  1. نكون أنشأنا كائنًا جديدًا.
  2. نكون شغّلنا (خلف الكواليس) الباني وممرًا له الوسطاء المناسبة، وإسناد هذه الوسطاء للمتغيرات المناسبة. هنا أُسندت الوسيط "John" إلى المتغير name عبر this.name.

…وبعد ذلك، ننادي توابِع الكائن مثل user.sayHi()‎.

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

ما الصنف أصلًا؟

هذا جميل، ولكن ما هو الصنف class أساسًا؟ لو ظننته كيانًا جديدًا كليًا في اللغة، فأنت مخطئ.

هيًا معًا نكشف الأسرار ونعرف ماهيّة الصنف حقًا، بذلك نفهم أمور كثيرة معقّدة، بسهولة.

الصنف -في جافاسكربت- مشابه للدالة نوعًا ما.

انظر بنفسك:

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// الإثبات: الصنف‫ User هو دالّة
alert(typeof User); // function

إليك ما يفعله الباني class User {...}‎ حقًا:

  1. يُنشِئ دالّة باسم User تصير هي ناتج التصريح عن الصنف، وتُؤخذ شيفرة الدالّة من الباني constructor (يُعامل الباني الصنف على أنّه فارغ إن لم تكن كتبت دالّة تغيّر ذلك).
  2. يُخزّن توابِع الصنف (مثل sayHi) في النموذج الأولي للكائن User.prototype.

متى ما أُنشأ كائن جديد (new User)، واستدعينا تابِعًا منه يُؤخذ التابِع من كائن النموذج الأولي (prototype) كما وضّحنا في الفصل ”الوراثة النموذجية - 2 -“. هكذا يكون للكائن تصريح الوصول إلى توابِع الصنف.

يمكن أن نوضّح ناتج التصريح class User في هذه الصورة:

class-user.png

إليك الشيفرة …:

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// الصنف ما هو إلّا دالةً
alert(typeof User); // function

// ...أو للدّقة هو تابِع الباني
alert(User === User.prototype.constructor); // true

// التوابِع موجودة في كائن النموذج الأولي ‫User.prototype، مثال:
alert(User.prototype.sayHi); // alert(this.name);

// في كائن النموذج الأولي ‫prototype تابِعين بالضبط
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

ليست مجرد تجميل لغوي

يقول البعض بأنّ الأصناف class هي مجرد "تجميل لغوي" (Syntactic sugar)، أي أنّها صياغة أساس تصميمها هو تسهيل القراءة دون تقديم ما هو جديد، إذ يمكننا ببساطة التصريح عن ذات الشيء بدون تلك الكلمة المفتاحية class:

// نُعيد كتابة صنف المستخدم ‫User باستعمال الدوال فقط

// ‫1. نُنشِئ دالّة الباني
function User(name) {
  this.name = name;
}
//عادةً إن أي نموذج أولي لتابِعٍ معين لديه باني
// لذلك لسنا بحاجة لإنشائه

// ‫2. نضيف التابع إلى النموذج الأولي
User.prototype.sayHi = function() {
  alert(this.name);
};

// طريقة الاستخدام:
let user = new User("John");
user.sayHi();

ناتج هذا التصريح أعلاه يشبه كثيرًا… لا بل يتطابق مع تصريح الصنف. لهذا ففكرة أنّ الأصناف هي حقًا تجميل لغوي لتعريف البواني مع توابِع كائن النموذج الأولي prototype لها - هي فكرة منطقية حقًا.

مع ذلك، فهناك فوارق.

  1. أولًا، تُوضع على الدوال الّتي تُنشؤها عبارة class كخاصيّةً داخلية فريدة لهذا الصنف هكذا ‎[[FunctionKind]]:"classConstructor"‎. لذا فالطريقتين (هذه وإنشائها يدويًا) ليستا تمامًا الشيء نفسه.

    وعلى عكس الدوالّ العادية، يلزم عليك استدعاء باني الصنف باستعمال new:

    class User {
      constructor() {}
    }
    
    alert(typeof User); // function
    User(); // Error: Class constructor User cannot be invoked without 'new'

    كما وأنّ تمثيل أغلب محرّكات جافاسكربت النصّيّ لباني الصنف يبدأ بالعبارة ”class…‎“

    class User {
      constructor() {}
    }
    
    alert(User); // class User { ... }

     

  2. توابِع الأصناف غير قابلة للإحصاء. يضبط التصريح عن الصنف راية enumerable على القيمة false لكلّ التوابِع في كائن النموذج الأولي للصنف "prototype".

    هذا جميل إذ لا نريد ظهور توابِع الصنف حين نستعرضهن باستعمال حلقة for..in.

  3. تستعمل الأصناف الوضع الصارم دومًا. كلّ الشيفرة البرمجية في الباني تكون بالوضع الصارم (سبق وأن تحدثنا في مقال سابق عن الوضع الصارم في لغة جافاسكربت).

كما أنّ صياغة الأصناف تُفيدنا بكثير من المزايا نشرحها لاحقًا.

تعابير الأصناف

كما الدوال فيمكن التعريف عن الأصناف داخل التعابير الأخرى وتمريرها وإعادتها وإسنادها وغيره.

إليك مثال عن تعبير صنف:

let User = class {
  sayHi() {
    alert("Hello");
  }
};

وكما تعابير الدوال المسمّاة (NFE) وهي اختصارًا لِـ (Named Function Expressions)، يمكن أن نضع اسمًا لتعابير الأصناف.

ولو كان لتعبير الصنف اسمًا فسيكون ظاهرًا داخل الصنف فقط لا غير:

// ”تعبير أصناف مسمّى“ ‫(NCE)
// ‫(ليس في المواصفات القياسية للغة هذا الاسم، لكنّها شبيهة بتعابير الدوال المسمّاة(NFE) )
let User = class MyClass {
  sayHi() {
    alert(MyClass); // لا يظهر اسم الصنف ‫MyClass إلّا داخل الصنف
  }
};

new User().sayHi(); // يعمل ويعرض تعريف ‫MyClass

alert(MyClass); // خطأ اسم تعبير الصنف ‫MyClass غير مرئي خارج الصنف

يمكننا حتى جعل الأصناف جاهزة عند الطلب، هكذا:

function makeClass(phrase) {
  // declare a class and return it
  return class {
    sayHi() {
      alert(phrase);
    };
  };
}

// إنشاء صنف جديد
let User = makeClass("Hello");

new User().sayHi(); // Hello

الجوالِب والضوابِط والاختصارات الأخرى

كما الكائنات المجردة فيمكن أن يكون في الأصناف ضوابِط وجوالِب (Gettes/Setters) وأسماءً محسوبةً للخاصيات (computed properties name) وغيرها.

إليك مثالًا عن خاصية user.name كتبنا تنفيذها عبر ضابِط وجالِب:

class User {

  constructor(name) {
    // يشغّل الضابِط
    this.name = name;
  }

  // هنا
  get name() {
    return this._name;
  }

  // وهنا
  set name(value) {
    if (value.length < 4) {
      alert("Name is too short."); // الاسم قصير جدًا
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // الاسم قصير جدًا

ينشئ التعريف عن الصنف جوالِب وضوابِط في كائن User.prototype هكذا:

Object.defineProperties(User.prototype, {
  name: {
    get() {
      return this._name
    },
    set(name) {
      // ...
    }
  }
});

وهذا مثال عن استخدام أسماءً محسوبةً للخاصيّة (computed property name) داخل أقواس [...]:

class User {

  // هنا
  ['say' + 'Hi']() {
    alert("Hello");
  }

}

new User().sayHi();

خاصيات الأصناف

تحذير: قد يكون عليك تعويض النقص في المتصفّحات القديمة الخاصيات على مستوى الأصناف هي إضافة حديثة على اللغة.

نرى في المثال أعلاه بأنّ للصنف User توابِع فقط. هيًا نُضف الخاصية name للصنف class User:

class User {
  name = "Anonymous"; // هكذا

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi();

alert(User.prototype.sayHi); // ‫موجود في كائن User.prototype
alert(User.prototype.name); // ‫undefined، غير موجودة في كائن User.prototype

الأمر الجدير بالذكر أن خاصيات الصنف تعيّن على أنها كائنات فردية وليست ضمن النموذج الأولي للصنف User.prototype. من الناحية العملية تُعالجُ هذه الخاصيات بعد أن يُنهي الباني عمله.

إنشاء توابع مرتبطة بخاصيات الصنف

كما تحدثنا في فصلٍ سابق من هذه السلسلة ربط الدوالّ، إن دلالات الكلمة المفتاحية this في الدوالّ ديناميكية إذ تعتمد إعتمادً أساسيًا على سياق الاستدعاء.

لذا فإن مررها تابِعٌ من كائنٍ معين للكلمة المفتاحية this، واستدعيت مرة أخرى في مكان آخر بغير السياق السابق، فلن تشير إلى نفس الكائن السابق بسبب تغير سياق الاستدعاء.

على سبيل المثال الشيفرة البرمجية التالية ستظهر undefined.

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined

تدعى هذه المشكلة "ضياع قيمة this". وهنالك طريقتين لإصلاح هذه المشكلة كما ذكرنا في فصل "ربط الدوال"ّ

  1. تمرير دالّة مغلّفة (مثل: setTimeout(() => button.click(), 1000)‎).
  2. ربط الدالّة بصنف كما في الباني التالي:
class Button {
  constructor(value) {
    this.value = value;
    this.click = this.click.bind(this);
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello

تزودنا خاصيات الصنف بصياغة مناسبة للحل الّذي سنستخدمه:

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello


تنشئ الخاصية click=‎ ()‎ =>‎ {...}‎ دالّة مستقلة لكل كائن من Button، والكلمة المفتاحية this تشير إلى الكائن نفسه. وبإمكاننا بعدها تمرير button.click في أي مكان ومع احتفاظها بالقيمة الصحيحة للكلمة المفتاحية this.

إن استخدام هذه الطريقة مفيد جدًا في بيئة المتصفحات وخصيصًا عند احتياجنا لإعداد دالّة مستمع الحدث (event listener).

خلاصة

الصياغة الأساسية للأصناف هي كالآتي:

class MyClass {
  prop = value; // خاصية

  constructor(...) { // الباني
    // ...
  }

  method(...) {} // تابِع

  get something(...) {} // تابِع جلب
  set something(...) {} // تابِع ضبط

  [Symbol.iterator]() {} // تابِع اسمه محسوب (نضع رمزًا هنا)‫  
  // ...
}

تقنيًا، فالصنف MyClass ما هو إلّا دالّة (تلك الّتي نكتبها لتكون الباني constructor)، بينما التوابِع والضوابِط والجوالِب تُكتب في كائن MyClass.prototype.

سنتعرّف أكثر في الفصول اللاحقة عن الأصناف والوراثة والميزات الأخرى.

تمارين

أعِد كتابتها لتكون صنفًا

الأهمية: 5

كُتب صنف الساعة Clock وكأنّه دالّة. أعِد كتابته ليكون بصياغة الأصناف.

ملاحظة: تدقّ عقارب الساعة في الطرفية، افتحها واحترس من العقارب اللادغة. اطلع على تجربة حية للتمرين.

الحل

class Clock {
  constructor({ template }) {
    this.template = template;
  }

  render() {
    let date = new Date();

    let hours = date.getHours();
    if (hours < 10) hours = '0' + hours;

    let mins = date.getMinutes();
    if (mins < 10) mins = '0' + mins;

    let secs = date.getSeconds();
    if (secs < 10) secs = '0' + secs;

    let output = this.template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs);

    console.log(output);
  }

  stop() {
    clearInterval(this.timer);
  }

  start() {
    this.render();
    this.timer = setInterval(() => this.render(), 1000);
  }
}


let clock = new Clock({template: 'h:m:s'});
clock.start();

ويمكن الاطلاع على تجربة حية للحل.

ترجمة -وبتصرف- للفصل Class basic syntax من كتاب The JavaScript Language





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


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



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

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

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


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

تسجيل الدخول

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


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