JavaScript قابلیتهای برنامهنویسی شیءگرا (OOP) را دارد. در برنامهنویسی شیءگرا میخواهید اشیائی («نمونهها») را از «قالبها» («کلاسها») بسازید تا دادهها و کارکردهای مشخصی داشته باشند. ویژگیهای داده در بافت شیءگرا «فیلد» و ویژگیهای تابعی «متد» نامیده میشوند.
JavaScript پیش از آنکه در سال ۲۰۱۵ به مشخصات زبان اضافه شوند، اصلاً کلاس نداشت، اما با «وراثت» مبتنی بر «پروتوتایپ» امکان برنامهنویسی شیءگرا را فراهم میکرد.
و حتی با وجود اینکه امروزه کلیدواژهی class در دسترس است، JavaScript هنوز زبانی مبتنی بر پروتوتایپ است.
برای درک اینکه زبان مبتنی بر پروتوتایپ بودن به چه معناست و JavaScript در واقع چطور کار میکند، به زمانی برمیگردیم که هنوز کلاسی وجود نداشت.
در JavaScript، برای قالب (کلاس) از یک تابع معمولی استفاده میشود.
وقتی قرار است تابعی به عنوان چنین قالبی استفاده شود، به آن «تابع سازنده» میگویند و قرارداد این است که اسم تابع با حرف بزرگ شروع شود.
نمونهها (اشیاء) هنگام فراخوانی تابع سازنده با کلیدواژهی new از قالب مشتق میشوند.
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
توجه به این نکته مهم است که در JavaScript، نمونهها و تابع سازنده حتی پس از ساختهشدن نمونهها رابطهای با هم حفظ میکنند.
هر شیء نمونه شامل یک ویژگی پنهان و داخلی است که در مشخصات زبان [[prototype]] نامیده میشود.
این ویژگی ارجاعی به مقدار کلید prototype تابع سازنده نگه میدارد.
بله، درست خواندید، یک تابع 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 آن متد را در سراسر زنجیرهی پروتوتایپ جستوجو میکند.
توجه داشته باشید که زنجیرهی پروتوتایپ فقط هنگام بازیابی یک مقدار طی میشود. تنظیم مستقیم یک ویژگی یا حذف یک ویژگی از شیء نمونه فقط همان نمونهی خاص را هدف میگیرد. اگر به زبانی با وراثت مبتنی بر کلاس عادت داشته باشید، ممکن است این انتظار را نداشته باشید.
امروزه 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.
نمونههای 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 حل کنید.
یک کلاس (تابع سازنده) به اسم 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 با دو فیلد 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
کلاس 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 که در محیطهای مرورگر وجود دارد متمایز شود.
کلاس 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
علاوه بر قابلیت تغییر اندازه، کلاس 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
تابعی به اسم changeWindow پیادهسازی کنید که یک نمونهی ProgramWindow را به عنوان ورودی میپذیرد و پنجره را به اندازه و موقعیت مشخصشده تغییر میدهد.
این تابع باید همان نمونهی ProgramWindow را که به آن داده شده، پس از اعمال تغییرات برگرداند.
پنجره باید عرض ۴۰۰ و ارتفاع ۳۰۰ بگیرد و در x = ۱۰۰ و y = ۱۵۰ قرار بگیرد.
const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400
// Similar for the other fields.
در Exercism ثبتنام کنید تا JavaScript را همراه با 37 مفهوم159 تمرین و مربیگری انسانی واقعی یاد بگیرید و در آن استاد شوید، همهی اینها رایگان.