نظام النوافذ

نظام النوافذ

تمرين تعلّمي

مقدمة

توفّر JavaScript إمكانات البرمجة كائنية التوجه (OOP). في البرمجة كائنية التوجه، تريد إنشاء كائنات (نسخ) من «قوالب» (أصناف) بحيث تتضمن بيانات ووظائف معيّنة. تُسمّى خصائص البيانات حقولًا في سياق البرمجة كائنية التوجه، وتُسمّى خصائص الدوال طُرقًا.

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

لفهم معنى أن تكون اللغة قائمة على النموذج الأولي، وكيف تعمل JavaScript فعليًا، سنعود إلى زمن لم تكن فيه أصناف.

بنية النموذج الأولي

دالة المُنشئ

في JavaScript، يُوفَّر القالب (الصنف) عبر دالة عادية. وعندما يُفترض استخدام دالة كقالب من هذا النوع، فإنها تُسمّى دالة المُنشئ، والعُرف أن يبدأ اسم الدالة بحرف كبير. وتُشتقّ النسخ (الكائنات) من القالب باستخدام الكلمة المفتاحية new عند استدعاء دالة المُنشئ.

function Car() {
  // ...
}

const myCar = new Car();
const yourCar = new Car();

من المهم ملاحظة أن النسخ ودالة المُنشئ تحتفظ في JavaScript بعلاقة بينها حتى بعد إنشاء النسخ. يتضمن كل كائن نسخة خاصية داخلية مخفية يُشار إليها باسم [[prototype]] في مواصفات اللغة. وهي تحمل مرجعًا إلى قيمة المفتاح prototype في دالة المُنشئ. نعم، لقد قرأت ذلك صحيحًا: يمكن لدالة JavaScript أن تمتلك أزواج مفتاح/قيمة لأنها أيضًا كائن من وراء الكواليس.

Note

للتلخيص:

  • دوال المُنشئ في 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 تبحث عن تلك الطريقة في سلسلة النموذج الأولي بأكملها.

Caution

لاحظ أن سلسلة النموذج الأولي لا تُسلك إلا عند استرجاع قيمة. فتعيين خاصية مباشرة أو حذف خاصية من كائن نسخة يستهدف تلك النسخة بعينها فقط. وقد لا يكون هذا ما تتوقعه إذا اعتدت على لغة ذات وراثة قائمة على الأصناف.

بنية الصنف

في الوقت الحاضر، تدعم 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.

  1. أمثلة على 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 والمهام المتبقية باستخدام صيغة الصنف.

1. عرّف 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

2. عرّف 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

3. عرّف صنف ProgramWindow

عرّف صنف ProgramWindow بالحقول التالية:

  • screenSize: يحمل قيمة ثابتة من النوع Size، حيث width هي 800 وheight هي 600
  • size : يحمل قيمة من النوع Size، وقيمته الابتدائية هي القيمة الافتراضية لكائن Size
  • position : يحمل قيمة من النوع Position، وقيمته الابتدائية هي القيمة الافتراضية لكائن Position

عند فتح النافذة (إنشائها)، تكون دائمًا بالحجم والموضع الافتراضيين في البداية.

const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800

// Similar for the other fields.

ملاحظة جانبية: استُخدم الاسم ProgramWindow بدلًا من Window للتمييز بين الصنف والصنف Window المدمج الموجود في بيئات المتصفح.

4. أضف طريقة لتغيير حجم النافذة

يجب أن يتضمن صنف ProgramWindow طريقة resize. يجب أن تقبل معاملًا من النوع Size كمدخل وتحاول تغيير حجم النافذة إلى الحجم المحدد.

غير أن الحجم الجديد لا يمكنه أن يتجاوز حدودًا معينة.

  • الحد الأدنى المسموح به للارتفاع أو العرض هو 1. أي ارتفاع أو عرض مطلوب أقل من 1 سيُقصّ إلى 1.
  • يعتمد الحد الأقصى للارتفاع والعرض على الموضع الحالي للنافذة، فلا يمكن لحواف النافذة أن تتجاوز حواف الشاشة. القيم الأكبر من هذه الحدود ستُقصّ إلى أكبر حجم يمكن أن تتخذه. على سبيل المثال، إذا كان موضع النافذة عند 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

5. أضف طريقة لتحريك النافذة

بالإضافة إلى وظيفة تغيير الحجم، يجب أن يتضمن صنف ProgramWindow أيضًا طريقة move. يجب أن تقبل معاملًا من النوع Position كمدخل. تشبه طريقة move الطريقة resize، غير أن هذه الطريقة تضبط موضع النافذة على القيمة المطلوبة بدلًا من الحجم.

وكما في resize، لا يمكن للموضع الجديد أن يتجاوز حدودًا معينة.

  • أصغر موضع هو 0 لكلٍّ من 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

6. غيّر نافذة البرنامج

نفّذ دالة changeWindow تقبل كائن ProgramWindow كمدخل وتغيّر النافذة إلى الحجم والموضع المحددين. يجب أن تُرجع الدالة كائن ProgramWindow الذي مُرِّر إليها بعد تطبيق التغييرات.

يجب أن تحصل النافذة على عرض 400 وارتفاع 300 وأن توضع عند x = 100 وy = 150.

const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400

// Similar for the other fields.
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
JavaScript Exercism

مستعد لبدء نظام النوافذ؟

سجّل في Exercism لتتعلّم وتتقن JavaScript عبر 37 مفهومًا159 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.