اقتباسفي البرمجة كائنية التوجّه، يعدّ الصنف عبارة عن توسعة للشيفرة البرمجية للبرنامج وذلك لإنشاء كائنات كما أنه يهيئ القيم الأولية لمتغيرات الحالة (أو تدعى أحيانًا خاصيات وهي المتغيرات الّتي تكون عضوًا بالصنف) وتطبيق سلوك معين (باستخدام الدوالّ أو التوابِع الأعضاء) - ويكيبيديا
بعيدًا عن الكلام النظري، ففي الواقع نريد عادةً إنشاء أكثر من كائن تحمل نفس النوع، مثل المستخدمين والبضائع وغيرها.
كما علمنا في الفصل "الباني والعامل "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")
:
- نكون أنشأنا كائنًا جديدًا.
-
نكون شغّلنا (خلف الكواليس) الباني وممرًا له الوسطاء المناسبة، وإسناد هذه الوسطاء للمتغيرات المناسبة. هنا أُسندت الوسيط "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 {...}
حقًا:
-
يُنشِئ دالّة باسم
User
تصير هي ناتج التصريح عن الصنف، وتُؤخذ شيفرة الدالّة من البانيconstructor
(يُعامل الباني الصنف على أنّه فارغ إن لم تكن كتبت دالّة تغيّر ذلك). -
يُخزّن توابِع الصنف (مثل
sayHi
) في النموذج الأولي للكائنUser.prototype
.
متى ما أُنشأ كائن جديد (new User
)، واستدعينا تابِعًا منه يُؤخذ التابِع من كائن النموذج الأولي (prototype) كما وضّحنا في الفصل ”الوراثة النموذجية - 2 -“. هكذا يكون للكائن تصريح الوصول إلى توابِع الصنف.
يمكن أن نوضّح ناتج التصريح class User
في هذه الصورة:
إليك الشيفرة …:
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 لها - هي فكرة منطقية حقًا.
مع ذلك، فهناك فوارق.
-
أولًا، تُوضع على الدوال الّتي تُنشؤها عبارة
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 { ... }
-
توابِع الأصناف غير قابلة للإحصاء. يضبط التصريح عن الصنف راية
enumerable
على القيمةfalse
لكلّ التوابِع في كائن النموذج الأولي للصنف"prototype"
.هذا جميل إذ لا نريد ظهور توابِع الصنف حين نستعرضهن باستعمال حلقة
for..in
. -
تستعمل الأصناف الوضع الصارم دومًا. كلّ الشيفرة البرمجية في الباني تكون بالوضع الصارم (سبق وأن تحدثنا في مقال سابق عن الوضع الصارم في لغة جافاسكربت).
كما أنّ صياغة الأصناف تُفيدنا بكثير من المزايا نشرحها لاحقًا.
تعابير الأصناف
كما الدوال فيمكن التعريف عن الأصناف داخل التعابير الأخرى وتمريرها وإعادتها وإسنادها وغيره.
إليك مثال عن تعبير صنف:
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". وهنالك طريقتين لإصلاح هذه المشكلة كما ذكرنا في فصل "ربط الدوال"ّ
-
تمرير دالّة مغلّفة (مثل:
setTimeout(() => button.click(), 1000)
). - ربط الدالّة بصنف كما في الباني التالي:
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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.