توفّر JavaScript إمكانات البرمجة كائنية التوجه (OOP). في البرمجة كائنية التوجه، تريد إنشاء كائنات (نسخ) من «قوالب» (أصناف) بحيث تتضمن بيانات ووظائف معيّنة. تُسمّى خصائص البيانات حقولًا في سياق البرمجة كائنية التوجه، وتُسمّى خصائص الدوال طُرقًا.
لم تكن JavaScript تمتلك أصنافًا على الإطلاق قبل إضافتها إلى مواصفات اللغة في 2015، لكنها كانت تتيح البرمجة كائنية التوجه باستخدام الوراثة القائمة على النموذج الأولي.
ورغم توفّر الكلمة المفتاحية class اليوم، فإن JavaScript لا تزال لغة قائمة على النموذج الأولي.
لفهم معنى أن تكون اللغة قائمة على النموذج الأولي، وكيف تعمل JavaScript فعليًا، سنعود إلى زمن لم تكن فيه أصناف.
في JavaScript، يُوفَّر القالب (الصنف) عبر دالة عادية.
وعندما يُفترض استخدام دالة كقالب من هذا النوع، فإنها تُسمّى دالة المُنشئ، والعُرف أن يبدأ اسم الدالة بحرف كبير.
وتُشتقّ النسخ (الكائنات) من القالب باستخدام الكلمة المفتاحية new عند استدعاء دالة المُنشئ.
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
من المهم ملاحظة أن النسخ ودالة المُنشئ تحتفظ في JavaScript بعلاقة بينها حتى بعد إنشاء النسخ.
يتضمن كل كائن نسخة خاصية داخلية مخفية يُشار إليها باسم [[prototype]] في مواصفات اللغة.
وهي تحمل مرجعًا إلى قيمة المفتاح prototype في دالة المُنشئ.
نعم، لقد قرأت ذلك صحيحًا: يمكن لدالة JavaScript أن تمتلك أزواج مفتاح/قيمة لأنها أيضًا كائن من وراء الكواليس.
للتلخيص:
prototype للمُنشئ (الدالة) هي النموذج الأولي للنسخة.غالبًا ما تريد أن تتضمن كل الكائنات المشتقّة (النسخ) بعض الحقول، وأن تمرّر لها بعض القيم الأولية عند إنشاء الكائن.
ويمكن تحقيق ذلك عبر الكلمة المفتاحية this.
داخل دالة المُنشئ، يمثّل this الكائن الجديد الذي سيُنشأ عبر new.
ويُرجع this تلقائيًا من دالة المُنشئ عند استدعائها بـ new.
وهذا يعني أننا نستطيع إضافة حقول إلى النسخة الجديدة بإضافتها إلى this داخل دالة المُنشئ.
function Car(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
تُضاف الطُرق عبر خاصية prototype الخاصة بدالة المُنشئ.
وداخل الطريقة، يمكنك الوصول إلى حقول النسخة عبر this.
وهذا يعمل بفضل القاعدة العامة التالية.
عندما تُستدعى دالة كطريقة لكائن، فإن
thisالخاصة بها تُضبط على الكائن الذي استُدعيت عليه الطريقة. 1
function Car() {
this.engineRunning = false;
// ...
}
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
Car.prototype.addGas = function (litre) {
// ...
};
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
myCar في المثال أعلاه كائن JavaScript عادي، ولو فحصنا (مثلًا في طرفية المتصفح) فلن نجد خاصية startEngine بقيمة دالة مباشرة داخل كائن myCar.
فكيف يعمل الكود أعلاه إذن؟
السر هنا يُسمّى سلسلة النموذج الأولي.
عندما تحاول الوصول إلى أي خاصية (حقل أو طريقة) لكائن، تتحقق JavaScript أولًا مما إذا كان المفتاح المعني موجودًا مباشرة في الكائن نفسه.
وإن لم يكن، تواصل البحث عن المفتاح في الكائن الذي تشير إليه خاصية [[prototype]] في الكائن الأصلي.
وكما ذُكر سابقًا، تشير [[prototype]] في مثالنا إلى خاصية prototype الخاصة بدالة المُنشئ.
وهناك ستجد JavaScript دالة startEngine لأننا أضفناها هناك.
function Car() {
// ...
}
Car.prototype.startEngine = function () {
// ...
};
ولا تتوقف السلسلة عند هذا الحد.
تشير خاصية [[prototype]] في Car.prototype (myCar.[[prototype]].[[prototype]]) إلى Object.prototype (خاصية prototype الخاصة بدالة المُنشئ Object).
وهي تحتوي على طُرق عامة متاحة لجميع كائنات JavaScript، مثل toString().
وخلاصة القول، يمكنك استدعاء myCar.toString() وستكون تلك الطريقة موجودة لأن JavaScript تبحث عن تلك الطريقة في سلسلة النموذج الأولي بأكملها.
لاحظ أن سلسلة النموذج الأولي لا تُسلك إلا عند استرجاع قيمة. فتعيين خاصية مباشرة أو حذف خاصية من كائن نسخة يستهدف تلك النسخة بعينها فقط. وقد لا يكون هذا ما تتوقعه إذا اعتدت على لغة ذات وراثة قائمة على الأصناف.
في الوقت الحاضر، تدعم JavaScript تعريف الأصناف بالكلمة المفتاحية class.
وقد أُضيفت هذه الميزة إلى مواصفات اللغة في 2015.
من ناحية، يوفّر هذا سكرًا نحويًا يجعل الأصناف أسهل في القراءة والكتابة.
فالبنية الجديدة أشبه بطريقة كتابة الأصناف في لغات مثل C++ أو Java.
ويجد المطوّرون القادمون من تلك اللغات سهولة أكبر في التكيّف.
ومن ناحية أخرى، تُمهّد بنية الأصناف الطريق لميزات لغوية جديدة غير متاحة في بنية النموذج الأولي.
في البنية الجديدة، تُعرَّف الأصناف بالكلمة المفتاحية class، يتبعها اسم الصنف ثم جسم الصنف بين أقواس معقوفة.
ويحتوي الجسم على تعريف دالة المُنشئ، أي طريقة خاصة باسم constructor.
وتعمل هذه الدالة تمامًا مثل دالة المُنشئ في بنية النموذج الأولي.
كما يحتوي جسم الصنف على جميع تعاريف الطُرق.
وبنية الطُرق مشابهة للصيغة المختصرة التي رأيناها لإضافة الدوال كقيم داخل كائن، انظر مفهوم الكائنات.
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
startEngine() {
this.engineRunning = true;
}
addGas(litre) {
// ...
}
}
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
تذكّر أنه من وراء الكواليس، لا تزال JavaScript لغة قائمة على النموذج الأولي. وكل الآليات التي تعرّفنا عليها في قسم «بنية النموذج الأولي» أعلاه لا تزال سارية.
افتراضيًا، تكون جميع حقول النسخة عامة في JavaScript. ويمكن الوصول إليها وإسناد قيم إليها مباشرة.
غير أن هناك عُرفًا راسخًا بأن الحقول والطُرق التي تبدأ بشرطة سفلية ينبغي التعامل معها على أنها خاصة. فلا ينبغي الوصول إليها مباشرة من خارج الصنف.
ترافق الحقول الخاصة أحيانًا دوال الجلب ودوال التعيين.
وبالكلمتين المفتاحيتين get وset يمكنك تعريف دوال تُنفَّذ عند الوصول إلى خاصية تحمل الاسم نفسه أو الإسناد إليها.
class Car {
constructor() {
this._mileage = 0;
}
get mileage() {
return this._mileage;
}
set mileage(value) {
throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.
أمثلة على this - كطريقة لكائن، MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (بالإشارة إلى 03 ديسمبر 2021) ↩
في هذا التمرين، ستحاكي نظام حاسوب قائمًا على النوافذ. ستنشئ بعض النوافذ التي يمكن تحريكها وتغيير حجمها. الصورة التالية تمثّل القيم التي ستعمل عليها أدناه.
<--------------------- screenSize.width --------------------->
^ ┌────────────────────────────────────────────────────────────┐
| │ │
| │ position.x, _ │
| │ position.y \ │
| │ \<----- size.width -----> │
| │ ^ *──────────────────────┐ │
| │ | │ title │ │
| │ | ├──────────────────────┤ │
screenSize.height │ | │ │ │
| │ size.height │ │ │
| │ | │ contents │ │
| │ | │ │ │
| │ | │ │ │
| │ v └──────────────────────┘ │
| │ │
| │ │
v └────────────────────────────────────────────────────────────┘
📣 لممارسة مجموعة مهاراتك الواسعة في JavaScript، حاول حل المهمتين 1 و2 باستخدام صيغة prototype والمهام المتبقية باستخدام صيغة الصنف.
Size لتخزين أبعاد النافذةعرّف صنفًا (دالة بانية) باسم Size.
يجب أن يحتوي على حقلين width وheight يخزّنان الأبعاد الحالية للنافذة.
يجب أن تقبل دالة البناء قيمًا ابتدائية لهذين الحقلين.
يُمرَّر العرض بوصفه المعامل الأول، والارتفاع بوصفه الثاني.
القيمة الافتراضية للعرض والارتفاع هي 80 و60 على التوالي.
بالإضافة إلى ذلك، عرّف طريقة resize(newWidth, newHeight) تأخذ عرضًا وارتفاعًا جديدين بوصفهما معاملين وتغيّر الحقلين ليعكسا الحجم الجديد.
const size = new Size(1080, 764);
size.width;
// => 1080
size.height;
// => 764
size.resize(1920, 1080);
size.width;
// => 1920
size.height;
// => 1080
Position لتخزين موضع النافذةعرّف صنفًا (دالة بانية) باسم Position بحقلين، x وy، يخزّنان الموضع الأفقي والعمودي الحالي على التوالي للزاوية العلوية اليسرى للنافذة.
يجب أن تقبل دالة البناء قيمًا ابتدائية لهذين الحقلين.
تُمرَّر قيمة x بوصفها المعامل الأول، وقيمة y بوصفها الثاني.
القيمة الافتراضية هي 0 لكلا الحقلين.
الموضع (0، 0) هو الزاوية العلوية اليسرى للشاشة، حيث تكبر قيم x كلما تحركت إلى اليمين وتكبر قيم y كلما تحركت إلى الأسفل.
كذلك عرّف طريقة move(newX, newY) تأخذ معاملين جديدين x وy وتغيّر الخصائص لتعكس الموضع الجديد.
const point = new Position();
point.x;
// => 0
point.y;
// => 0
point.move(100, 200);
point.x;
// => 100
point.y;
// => 200
ProgramWindow
عرّف صنف ProgramWindow بالحقول التالية:
screenSize: يحمل قيمة ثابتة من النوع Size، حيث width هي 800 وheight هي 600size : يحمل قيمة من النوع Size، وقيمته الابتدائية هي القيمة الافتراضية لكائن Size
position : يحمل قيمة من النوع Position، وقيمته الابتدائية هي القيمة الافتراضية لكائن Position
عند فتح النافذة (إنشائها)، تكون دائمًا بالحجم والموضع الافتراضيين في البداية.
const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800
// Similar for the other fields.
ملاحظة جانبية: استُخدم الاسم ProgramWindow بدلًا من Window للتمييز بين الصنف والصنف Window المدمج الموجود في بيئات المتصفح.
يجب أن يتضمن صنف ProgramWindow طريقة resize.
يجب أن تقبل معاملًا من النوع Size كمدخل وتحاول تغيير حجم النافذة إلى الحجم المحدد.
غير أن الحجم الجديد لا يمكنه أن يتجاوز حدودًا معينة.
x = 400 وy = 300 وطُلب تغيير الحجم إلى height = 400 وwidth = 300، فسيُغيَّر حجم النافذة إلى height = 300 وwidth = 300 لأن الشاشة ليست كبيرة بما يكفي في اتجاه y لاستيعاب الطلب بالكامل.const programWindow = new ProgramWindow();
const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400
بالإضافة إلى وظيفة تغيير الحجم، يجب أن يتضمن صنف ProgramWindow أيضًا طريقة move.
يجب أن تقبل معاملًا من النوع Position كمدخل.
تشبه طريقة move الطريقة resize، غير أن هذه الطريقة تضبط موضع النافذة على القيمة المطلوبة بدلًا من الحجم.
وكما في resize، لا يمكن للموضع الجديد أن يتجاوز حدودًا معينة.
x وy.x = 250 وy = 100 وطُلب نقلها إلى x = 600 وy = 200، فسيُنقل النافذة إلى x = 550 وy = 200 لأن الشاشة ليست كبيرة بما يكفي في اتجاه x لاستيعاب الطلب بالكامل.const programWindow = new ProgramWindow();
const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100
نفّذ دالة changeWindow تقبل كائن ProgramWindow كمدخل وتغيّر النافذة إلى الحجم والموضع المحددين.
يجب أن تُرجع الدالة كائن ProgramWindow الذي مُرِّر إليها بعد تطبيق التغييرات.
يجب أن تحصل النافذة على عرض 400 وارتفاع 300 وأن توضع عند x = 100 وy = 150.
const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400
// Similar for the other fields.
سجّل في Exercism لتتعلّم وتتقن JavaScript عبر 37 مفهومًا159 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.