۲۵ ژوئیه ۲۰۲۲

وراثت کلاس

وراثت کلاس (class inheritance) راهی برای یک کلاس است تا کلاس دیگری را تعمیم دهد (extend).

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

کلمه کلیدی “extends”

فرض کنیم ما کلاس Animal (به معنی حیوان) را داریم:

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} با سرعت ${this.speed} می‌دود.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} ایستاده است.`);
  }
}

let animal = new Animal("جانور من");

ما می‌توانیم شیء animal و کلاس Animal را اینگونه به صورت هندسی نمایش دهیم:

…و ما می‌خواهیم که class Rabbit دیگری هم بسازیم.

چون خرگوش‌ها هم جانور هستند، کلاس Rabbit (به معنی خرگوش) باید بر اساس Animal باشد و به متدهای جانور دسترسی داشته باشد تا خرگوش‌ها بتوانند کاری که جانوران “معمولی” انجام می‌دهند را انجام دهند.

سینتکس تعمیم دادن کلاس اینگونه است: class Child extends Parent.

بیایید کلاس class Rabbit را بسازیم که از Animal ارث‌بری می‌کند:

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} قایم می‌شود!`);
  }
}

let rabbit = new Rabbit("خرگوش سفید");

rabbit.run(5); // خرگوش سفید با سرعت 5 می‌دود
rabbit.hide(); // !خرگوش سفید قایم می‌شود

شیء ساخته شده از کلاس Rabbit هم به متدهای Rabbit، مانند rabbit.hide()، دسترسی دارد و هم به متدهای Animal، مانند rabbit.run().

از دورن، کلمه کلیدی extends با استفاده از مکانیزم‌های خوب و قدیمی پروتوتایپ کار می‌کند. این کلمه Rabbit.prototype.[[Prototype]] را برابر با Animal.prototype قرار می‌دهد. پس اگر متدی در Rabbit.prototype پیدا نشد، جاوااسکریپت آن را از Animal.prototype دریافت می‌کند.

برای مثال، برای پیدا کردن متد rabbit.run، موتور این‌ها را بررسی می‌کند (در تصویر از پایین به بالا است):

  1. شیء rabbit (run ندارد).
  2. پروتوتایپ آن، یعنی Rabbit.prototype (hide را دارد، اما run را نه).
  3. پروتوتایپ آن، یعنی (با توجه به extends) همان Animal.prototype که در نهایت متد run را دارد.

همانطور که از فصل پروتوتایپ‌های نیتیو (Native prototypes) به یاد داریم، خود جاوااسکریپت از وراثت پروتوتایپی برای شیءهای درون‌ساخت استفاده می‌کند. برای مثال Date.prototype.[[Prototype]] برابر با Object.prototype است. به همین دلیل است که تاریخ‌ها به متدهای عمومی شیء دسترسی دارند.

هر عبارتی بعد از extends مجاز است

سینتکس کلاس اجازه می‌دهد که نه تنها یک کلاس بلکه هر عبارتی را بعد از extends قرار دهیم.

برای مثال، فراخوانی تابعی که کلاس والد را تولید می‌کند:

function f(phrase) {
  return class {
    sayHi() { alert(phrase); }
  };
}

class User extends f("سلام") {}

new User().sayHi(); // سلام

اینجا class User از نتیجه f("سلام") ارث‌بری می‌کند.

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

بازنویسی متد

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

اما اگر ما متد خودمان را درون Rabbit مشخص کنیم، مثل stop()، در عوض این متد استفاده خواهد شد:

class Rabbit extends Animal {
  stop() {
    // استفاده خواهد شد rabbit.stop() حالا این متد برای...
    // Animal از کلاس stop() به جای
  }
}

معمولا ما نمی‌خواهیم که یک متد والد را به طور کلی جایگزین کنیم بلکه می‌خواهیم متدی بر اساس آن بسازیم تا عملکرد آن را تغییر یا گسترش بدهیم. ما کاری را درون متد خود انجام می‌دهیم اما متد والد را قبل/بعد از آن یا در حین فرایند فراخوانی می‌کنیم.

کلاس‌ها کلمه کلیدی "super" را برای این موضوع فراهم می‌کنند.

  • متد super.method(...) برای فراخوانی یک متد والد
  • تابع super(...) برای فراخوانی سازنده والد (فقط درون سازنده خودمان).

برای مثال، بیایید بگذاریم خرگوش ما زمانی که می‌ایستد به طور خودکار قایم شود:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed = speed;
    alert(`${this.name} با سرعت ${this.speed} می‌دود.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} ایستاده است.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} قایم می‌شود!`);
  }

  stop() {
    super.stop(); // والد را فراخوانی کن stop متد
    this.hide(); // را فراخوانی کن hide و سپس
  }
}

let rabbit = new Rabbit("خرگوش سفید");

rabbit.run(5); // خرگوش سفید با سرعت 5 می‌دود
rabbit.stop(); // !خرگوش سفید ایستاده است. خرگوش سفید قایم می‌شود

حالا Rabbit متد stop را دارد که در فرایند خودش super.stop() والد را فراخوانی می‌کند.

تابع‌های کمانی super ندارند

همانطور که در فصل سرکشی دوباره از تابع‌های کمانی گفته شد، تابع‌های کمانی super ندارند.

اگر به آن دسترسی پیدا کنیم، از تابع بیرونی گرفته می‌شود. برای مثال:

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // والد بعد از 1 ثانیه stop فراخوانی
  }
}

مقدار super در تابع کمانی با مقدار آن در stop() یکسان است و همانطور که توقع می‌رود کار می‌کند. اگر ما یک تابع «معمولی» را اینجا مشخص کرده بودیم، ارور ایجاد می‌شد:

// غیرمنتظره super
setTimeout(function() { super.stop() }, 1000);