مسیرها
/
JavaScript
JavaScript
/
تمرین‌ها
/
سیستم پنجره‌بندی
سیستم پنجره‌بندی

سیستم پنجره‌بندی

تمرین یادگیری

مقدمه

JavaScript قابلیت‌های برنامه‌نویسی شیءگرا (OOP) را دارد. در برنامه‌نویسی شیءگرا می‌خواهید اشیائی («نمونه‌ها») را از «قالب‌ها» («کلاس‌ها») بسازید تا داده‌ها و کارکردهای مشخصی داشته باشند. ویژگی‌های داده در بافت شیءگرا «فیلد» و ویژگی‌های تابعی «متد» نامیده می‌شوند.

JavaScript پیش از آنکه در سال ۲۰۱۵ به مشخصات زبان اضافه شوند، اصلاً کلاس نداشت، اما با «وراثت» مبتنی بر «پروتوتایپ» امکان برنامه‌نویسی شیءگرا را فراهم می‌کرد. و حتی با وجود اینکه امروزه کلیدواژه‌ی 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 ساخته خواهد شد. وقتی تابع سازنده با new فراخوانی شود، this به‌طور خودکار از آن برگردانده می‌شود.

یعنی می‌توانیم با اضافه کردن‌شان به 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 پشتیبانی می‌کند. این قابلیت در سال ۲۰۱۵ به مشخصات زبان اضافه شد. از یک سو، این امر میانبری نگارشی فراهم می‌کند که خواندن و نوشتن کلاس‌ها را آسان‌تر می‌کند. نگارش جدید بیشتر شبیه روشی است که کلاس‌ها در زبان‌هایی مثل 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 (ارجاع: ۳ دسامبر ۲۰۲۱) ↩

دستورالعمل‌ها

در این تمرین، یک سیستم کامپیوتری پنجره‌محور را شبیه‌سازی می‌کنید. چند پنجره می‌سازید که می‌توان آن‌ها را جابه‌جا کرد و اندازه‌شان را تغییر داد. تصویر زیر نمونه‌ای از مقادیری است که در ادامه با آن‌ها کار می‌کنید.

                  <--------------------- screenSize.width --------------------->

       ^          ┌────────────────────────────────────────────────────────────┐
       |          │                                                            │
       |          │         position.x, _                                      │
       |          │         position.y   \                                     │
       |          │                       \<----- size.width ----->            │
       |          │                 ^      *──────────────────────┐            │
       |          │                 |      │        title         │            │
       |          │                 |      ├──────────────────────┤            │
screenSize.height │                 |      │                      │            │
       |          │            size.height │                      │            │
       |          │                 |      │       contents       │            │
       |          │                 |      │                      │            │
       |          │                 |      │                      │            │
       |          │                 v      └──────────────────────┘            │
       |          │                                                            │
       |          │                                                            │
       v          └────────────────────────────────────────────────────────────┘

📣 برای تمرین مهارت‌های متنوع JavaScript خود، کارهای ۱ و ۲ را با نحوه‌ی نگارش prototype و کارهای باقی‌مانده را با نحوه‌ی نگارش class حل کنید.

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 است.

موقعیت (۰، ۰) گوشه‌ی بالا-چپ صفحه است؛ هرچه به سمت راست بروید مقادیر 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 برابر ۸۰۰ و height برابر ۶۰۰ نگه می‌دارد
  • 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 را به عنوان ورودی می‌پذیرد و تلاش می‌کند اندازه‌ی پنجره را به اندازه‌ی مشخص‌شده تغییر دهد.

با این حال، اندازه‌ی جدید نمی‌تواند از محدوده‌های مشخصی فراتر برود.

  • کمترین ارتفاع یا عرض مجاز ۱ است. ارتفاع یا عرض درخواستی کمتر از ۱ به ۱ محدود می‌شود.
  • بیشترین ارتفاع و عرض به موقعیت فعلی پنجره بستگی دارد؛ لبه‌های پنجره نمی‌توانند از لبه‌های صفحه فراتر بروند. مقادیری که بزرگ‌تر از این محدوده‌ها باشند، به بزرگ‌ترین اندازه‌ای که می‌توانند بگیرند محدود می‌شوند. مثلاً اگر موقعیت پنجره در x = ۴۰۰ و y = ۳۰۰ باشد و تغییر اندازه‌ای به height = ۴۰۰ و width = ۳۰۰ درخواست شود، پنجره به height = ۳۰۰ و width = ۳۰۰ تغییر اندازه می‌یابد، چون صفحه در جهت 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، موقعیت جدید هم نمی‌تواند از محدوده‌های مشخصی فراتر برود.

  • کمترین موقعیت برای هر دو x و y برابر ۰ است.
  • بیشترین موقعیت در هر جهت به اندازه‌ی فعلی پنجره بستگی دارد. لبه‌ها نمی‌توانند از لبه‌های صفحه فراتر بروند. مقادیری که بزرگ‌تر از این محدوده‌ها باشند، به بزرگ‌ترین اندازه‌ای که می‌توانند بگیرند محدود می‌شوند. مثلاً اگر اندازه‌ی پنجره در x = ۲۵۰ و y = ۱۰۰ باشد و جابه‌جایی به x = ۶۰۰ و y = ۲۰۰ درخواست شود، پنجره به x = ۵۵۰ و y = ۲۰۰ منتقل می‌شود، چون صفحه در جهت 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 را که به آن داده شده، پس از اعمال تغییرات برگرداند.

پنجره باید عرض ۴۰۰ و ارتفاع ۳۰۰ بگیرد و در x = ۱۰۰ و y = ۱۵۰ قرار بگیرد.

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

// Similar for the other fields.
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
JavaScript Exercism

آماده‌اید سیستم پنجره‌بندی را شروع کنید؟

در Exercism ثبت‌نام کنید تا JavaScript را همراه با 37 مفهوم159 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.