مسیر
/
JavaScript
JavaScript
/
برنامه‌ی درسی
/
پروتوتایپ و کلاس
پر

پروتوتایپ و کلاس در JavaScript

32 تمرین

درباره‌ی پروتوتایپ و کلاس

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

جاوااسکریپت پیش از آنکه کلاس‌ها در سال ۲۰۱۵ به مشخصات زبان افزوده شوند اصلاً کلاس نداشت، اما برنامه‌نویسی شیءگرا را با وراثت مبتنی بر پروتوتایپ ممکن می‌کرد. و با اینکه امروزه کلیدواژه‌ی class در دسترس است، جاوااسکریپت هنوز زبانی مبتنی بر پروتوتایپ است.

برای درک اینکه مبتنی بر پروتوتایپ بودن یک زبان چه معنایی دارد و جاوااسکریپت در واقع چگونه کار می‌کند، به زمانی بازمی‌گردیم که کلاسی وجود نداشت.

نحوه‌ی نگارش پروتوتایپ

تابع سازنده

در جاوااسکریپت، قالب (کلاس) به کمک یک تابع معمولی ساخته می‌شود. وقتی قرار است تابعی به این شکل به عنوان قالب به کار رود، تابع سازنده نامیده می‌شود و قرارداد این است که اسم تابع باید با حرف بزرگ شروع شود. نمونه‌ها (اشیاء) هنگام فراخوانی تابع سازنده، با کلیدواژه‌ی new از قالب ساخته می‌شوند.

function Car() {
  // ...
}

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

توجه به این نکته مهم است که در جاوااسکریپت، نمونه‌ها و تابع سازنده حتی پس از ساخته شدن نمونه‌ها هم رابطه‌ی خود را با یکدیگر حفظ می‌کنند. هر شیء نمونه، یک ویژگی پنهان و درونی دارد که در مشخصات زبان [[prototype]] نامیده می‌شود. این ویژگی ارجاعی به مقدار کلید prototype تابع سازنده نگه می‌دارد. بله، درست خواندید؛ یک تابع جاوااسکریپت می‌تواند جفت‌های کلید/مقدار داشته باشد، چون در پشت صحنه خودش هم یک شیء است.

از سال ۲۰۱۵، می‌توان به [[prototype]] از طریق Object.getPrototypeOf() دسترسی داشت. پیش از آن، در بسیاری از محیط‌ها از طریق کلید __proto__ قابل دسترسی بود.

پروتوتایپ یک شیء ([[prototype]]) را با ویژگی prototype تابع سازنده اشتباه نگیرید.

Note

به طور خلاصه:

  • سازنده‌ها در جاوااسکریپت، توابع معمولی هستند.
  • ساختن یک نمونه‌ی جدید، شیئی می‌سازد که رابطه‌ای با سازنده‌ی خود دارد و به آن پروتوتایپ می‌گویند.
  • توابع، شیء هستند (اشیاء قابل فراخوانی) و بنابراین می‌توانند ویژگی داشته باشند.
  • ویژگی 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 در مثال بالا یک شیء معمولی جاوااسکریپت است و اگر آن را بررسی کنیم (مثلاً در کنسول مرورگر)، ویژگی startEngine را که مقدارش یک تابع باشد، مستقیماً درون شیء myCar پیدا نمی‌کنیم. پس کد بالا اصلاً چطور کار می‌کند؟

راز ماجرا اینجاست که به آن زنجیره‌ی پروتوتایپ می‌گویند. وقتی می‌خواهید به ویژگی‌ای (فیلد یا متد) از یک شیء دسترسی پیدا کنید، جاوااسکریپت ابتدا بررسی می‌کند که آیا کلید مربوطه مستقیماً در خود شیء پیدا می‌شود یا نه. اگر نه، جست‌وجوی کلید را در شیئی ادامه می‌دهد که ویژگی [[prototype]] شیء اصلی به آن ارجاع می‌دهد. همان‌طور که پیش‌تر گفته شد، در مثال ما [[prototype]] به ویژگی prototype تابع سازنده اشاره می‌کند. جاوااسکریپت تابع startEngine را همان‌جا پیدا می‌کند، چون آن را همان‌جا اضافه کرده‌ایم.

function Car() {
  // ...
}

Car.prototype.startEngine = function () {
  // ...
};

و زنجیره به همین‌جا ختم نمی‌شود. ویژگی [[prototype]] در Car.prototype (یعنی myCar.[[prototype]].[[prototype]]) به Object.prototype ارجاع می‌دهد (ویژگی prototype در تابع سازنده‌ی Object). Object.prototype شامل متدهای عمومی است که برای همه‌ی اشیاء جاوااسکریپت در دسترس‌اند، مثلاً toString(). [[prototype]] مربوط به Object معمولاً null است، بنابراین زنجیره‌ی پروتوتایپ همان‌جا تمام می‌شود. در نتیجه می‌توانید myCar.toString() را فراخوانی کنید و آن متد وجود خواهد داشت، چون جاوااسکریپت آن متد را در سراسر زنجیره‌ی پروتوتایپ جست‌وجو می‌کند. می‌توانید یک مثال مفصل را در مقاله‌ی MDN با عنوان «وراثت و زنجیره‌ی پروتوتایپ» ببینید.

Caution

توجه کنید که زنجیره‌ی پروتوتایپ فقط هنگام خواندن یک مقدار طی می‌شود. تنظیم مستقیم یک ویژگی یا حذف ویژگی یک شیء نمونه، فقط همان نمونه‌ی مشخص را هدف می‌گیرد. اگر به زبانی با وراثت مبتنی بر کلاس عادت داشته باشید، ممکن است این رفتار آن چیزی نباشد که انتظار دارید.

متدهای پویا (افزودن متد به همه‌ی نمونه‌های موجود)

جاوااسکریپت به شما اجازه می‌دهد متدهایی را حتی پس از ساخته شدن همه‌ی نمونه‌های موجود، به آن‌ها اضافه کنید.

یاد گرفتیم که هر نمونه ارجاعی به ویژگی prototype تابع سازنده نگه می‌دارد. یعنی اگر ورودی‌ای به آن شیء prototype اضافه کنید، آن ورودی جدید (مثلاً یک متد جدید) بلافاصله برای همه‌ی نمونه‌های ساخته‌شده از آن تابع سازنده در دسترس خواهد بود.

function Car() {
  this.engineRunning = false;
}

const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

myCar.startEngine();
// This works, even though myCar was created before the method
// was added.

در تئوری، می‌توان از متدهای پویا حتی برای گسترش قابلیت‌های اشیاء توکار مثل Object یا Array با تغییر پروتوتایپ آن‌ها استفاده کرد. به این کار مونکی پچینگ می‌گویند. چون این تغییر روی کل برنامه اثر می‌گذارد، برای پیشگیری از عوارض جانبی ناخواسته باید از آن پرهیز کرد. تنها کاربرد معقول آن، فراهم کردن یک polyfill برای متدی است که در محیط‌های قدیمی‌تر وجود ندارد.

نحوه‌ی نگارش کلاس

امروزه جاوااسکریپت از تعریف کلاس‌ها با کلیدواژه‌ی 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

مشابه اعلان‌های تابع و عبارت‌های تابع، جاوااسکریپت علاوه بر اعلان کلاس که در بالا دیدیم، از عبارت‌های کلاس هم پشتیبانی می‌کند.

به خاطر داشته باشید که جاوااسکریپت در پشت صحنه هنوز زبانی مبتنی بر پروتوتایپ است. همه‌ی سازوکارهایی که در بخش «نحوه‌ی نگارش پروتوتایپ» در بالا یاد گرفتیم، هنوز برقرارند.

فیلدهای خصوصی، گترها و سترها

به طور پیش‌فرض، همه‌ی فیلدهای نمونه در جاوااسکریپت عمومی هستند. می‌توان مستقیماً به آن‌ها دسترسی داشت و مقدارشان را تغییر داد.

افزودن فیلدهای خصوصی واقعی به مشخصات زبان هنوز در دست انجام است؛ برای جزئیات سند پیشنهاد را ببینید.

در این فاصله می‌توانید از قرارداد جاافتاده‌ای استفاده کنید که بر اساس آن، فیلدها و متدهایی که با زیرخط شروع می‌شوند باید خصوصی در نظر گرفته شوند. هرگز نباید مستقیماً از بیرون کلاس به آن‌ها دسترسی داشت.

فیلدهای خصوصی گاهی با گترها و سترها همراه می‌شوند. با کلیدواژه‌های 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.

فیلدهای کلاس و متدهای کلاس

در برنامه‌نویسی شیءگرا، گاهی می‌خواهید فیلدها یا متدهای کاربردی‌ای فراهم کنید که به نمونه‌ی مشخصی وابسته نیستند. در عوض، برای خود کلاس تعریف می‌شوند. این کار با کلیدواژه‌ی static انجام می‌شود.

class Car {
  static type = 'vehicle';

  static isType(targetType) {
    return targetType === 'vehicle';
  }
}

Car.type;
// => 'vehicle'

Car.isType('road sign');
// => false

وراثت مبتنی بر کلاس

علاوه بر نوع وراثت در طول زنجیره‌ی پروتوتایپ که پیش‌تر دیدیم، می‌توانید وراثت میان کلاس‌ها را هم در جاوااسکریپت نمایش دهید. این موضوع در مفهوم وراثت پوشش داده شده است.


  1. نمونه‌های this - به عنوان متد یک شیء، MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (ارجاع: ۰۳ دسامبر ۲۰۲۱) ↩

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

پروتوتایپ و کلاس را یاد بگیرید