بررسی جامع کلاس‌ها (Class) در جاوااسکریپت

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

درود دوستان. کلاس‌ها (Classes) از ES6 به جاوااسکریپت اومدن تا جایگزینی برای توابع Constructor باشن که یک روش سنتی برای ساختن آبجکت‌ها از یک طرح و الگوی اولیه به حساب میومد. این ویژگی کاربرد زیادی توی فریم‌ورک‌ها و کتابخونه‌هایی مثل ری‌اکت و انگولار داره.

موارد زیر رو توی این قسمت یاد می‌گیریم:

  • مشکل کجاست
  • کلاس چیه
  • ساختن آبجکت از کلاس‌ها
  • تعریف کردن متدها
  • متد constrcutor
  • تعریف کردن پراپرتی‌ها
  • دسترسی به فیلدها از متدها
  • کلاس‌های والد، کلاس‌های فرزند
  • دسترسی به اعضای کلاس والد
  • فرزند و زیرفرزند
  • بررسی کردن نوع آبجکت


مشکل کجاست؟

فرض کنیم آبجکتی داریم به صورت زیر:

const emily = {
  name: &quotEmily&quot,
  age: 4,
  run() {
    alert(`${this.name} is running`);
  }
}

همونطور که می‌بینیم ۲ پراپرتی داریم و یک متد. حالا می‌خوایم چند آبجکت دیگه مشابه همین آبجکت داشته باشیم که فقط توی مقدار پراپرتی‌ها با هم تفاوت دارن. شاید راهی که به ذهنمون برسه این باشه که هر آبجکتی رو دقیقاً مثل همون آبجکت بالا و به صورت دستی بسازیم:

const mario = {
  name: &quotMario&quot,
  age: 5,
  run() {
    alert(`${this.name} is running`);
  }
}

const john = {
  name: &quotJohn&quot,
  age: 3,
  run() {
    alert(`${this.name} is running`);
  }
}

// ...

این کار چند مشکل داره:

۱. داریم کدهای تکراری می‌نویسیم. متد run بدون تغییر توی همه آبجکت‌ها تکرار شده. کار زمانی سخت‌تر میشه که تعداد پراپرتی‌ها و متدهای ما زیاد باشن.

۲. برای اضافه کردن یک پراپرتی به آبجکت، باید سراغ همه آبجکت‌ها بریم و به صورت دستی پراپرتی‌ها رو اضافه کنیم.

کلاس‌ها برای حل چنین مشکل‌هایی به کارمون میان ?


کلاس چیه؟

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

توی جاوااسکریپت، یک کلاس با استفاده از کلمه‌کلیدی class ساخته میشه:

class User {

}

توی این کد، User اسم کلاس ما هست و هر چیزی که داخل براکت‌ها بنویسیم، بدنهٔ این کلاس به حساب میاد. توی این بدنه ما پراپرتی‌ها و متدهایی رو تعریف می‌کنیم تا توی آبجکت‌ها در دسترس باشن!


ساختن آبجکت از کلاس‌ها

ما با استفاده از کلمه‌کلیدی new که درست قبل از اسم کلاس قرار می‌گیره می‌تونیم آبجکت بسازیم:

class User { }

const emily = new User();
const mario = new User();

به آبجکت‌هایی که از یک کلاس ساخته میشه به اصطلاح می‌گیم نمونه یا Instance. الان متغیرهای emily و mario نمونه‌هایی هستن که هر دو از کلاس User ساخته شدن و دارای ویژگی‌هایی مشترکی هستن. اما هنوز هیچ ویژگی (پراپرتی و متد) تعریف نکردیم که توی این نمونه‌ها در دسترس باشن.


تعریف کردن متدها

متدها رو می‌تونیم به صورت زیر توی کلاس تعریف کنیم تا توی همه نمونه‌ها در دسترس باشن:

class User {
  welcome() {
    alert(&quotHello!&quot);
  }

  login() {
    alert(&quotI'm logging in&quot);
  }
}

const emily = new User();
const mario = new User();

emily.welcome(); // Hello!
mario.login(); // I'm logging in

همونطور که توی دو خط آخر می‌بینیم، متدهایی که توی کلاس تعریف کردیم توی نمونه‌ها در دسترس و قابل استفاده هستن.


متد constrcutor

همه کلاس‌ها می‌تونن یک متد اختصاصی داشته باشن به اسم constructor که به صورت زیر تعریف میشه:

class User {
  constructor() {
    // ...
  }
}

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

class User {
  constructor() {
    alert(&quotBravo&quot);
  }
}

const mario = new User(); // Bravo

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

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


تعریف کردن پراپرتی‌ها

برای اینکه بخوایم پراپرتی‌هایی داشته باشیم که توی همه نمونه‌ها در دسترس باشن از متد constructor و this کمک می‌گیریم:

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const mario = new User(&quotMario&quot, 5);
const emily = new User(&quotEmily&quot, 4);

alert(mario.name); // Mario
alert(mario.age); // 5
alert(emily.name); // Emily

توی خط ۲ برای constructor دو پارامتر تعریف کردیم و که اونها رو موقع ساختن نمونه (خط‌های ۸ و ۹) باید پاس بدیم. توی constructor با استفاده از کلمه‌کلیدی this تونستیم ۲ پراپرتی بسازیم و مقدار اونها رو برابر با ورودی‌های این متد قرار بدیم. از حالا نمونه‌هایی که ساخته میشن شامل این ۲ پراپرتی با مقدارهای اختصاصی خواهند بود و مثل خط‌های آخر قابل دسترس هستن ?


پراپرتی؟ فیلد؟ ?

شاید واژه فیلد (Field) هم به گوشتون خورده باشه که با واژه پراپرتی بجای هم مورد استفاده قرار می‌گیرن. توی کد بالا و توی خط ۳ و ۴ ما در واقع فیلد تعریف کردیم. این فیلد با مقدارش زمانی که توی نمونه‌ها قرار می‌گیرن، با عنوان پراپرتی شناخته میشن. پس پراپرتی چیزی هست که توی نمونه‌ها و فیلد چیزی هست که توی کلاس‌ها داریم. توی خط‌های آخر کد بالا داریم پراپرتی‌ها رو فراخونی می‌کنیم.


دسترسی به فیلدها توی متدها

با استفاده از کلمه‌کلیدی this می‌تونیم به فیلدها دسترسی داشته باشیم:

class User {
  constructor(name) {
    this.name = name;
  }

  welcome() {
    return `Welcome ${this.name}!`;
  }
}

const mario = new User(&quotMario&quot);
alert(mario.welcome()); // Welcome Mario!


کلاس‌های والد؟ کلاس‌های فرزند؟ ?

یک کلاس می‌تونه ویژگی‌های یک کلاس دیگه رو به ارث ببره. این کار که به اصطلاح به اون گفته میشه ارث‌بری، برای زمانی خوبه که می‌خوایم یک کلاس موجود رو توسعه (Extend) بدیم.

ابتدا فرض کنیم کلاسی داریم به صورت زیر:

class User {
  login() { }
  logout() { }
}

توی این کلاس که برای کاربرها هست ۲ تا متد قرار دادیم. حالا می‌خوایم برای کاربرها قابلیتی رو اضافه کنیم که بتونن پست ارسال کنن:

class User {
  login() { }
  logout() { }

  writePost() { }
  deletePost(id) { }
}

دو متد writePost و deletePost رو اضافه کردیم. اما قابلیت ارسال پست رو قصد نداریم در اختیار همه کاربرها قرار بدیم. چون کاربرها ممکنه نقش‌های متفاوتی مثل نقش‌ها مدیرتی داشته باشن که در این صورت این دو متد اضافه شده برای اونها بی‌معنی هست. همچنین اگه بخوایم برای نقش‌های مدیریتی متدهایی رو اضافه کنیم، این متدها برای کاربرهای نویسنده بی‌معنی هست.

یک راه بهتر اینه که برای کاربرهای نویسنده یا مدیر یک کلاس اختصاصی برای همون نقش درست کنیم و قابلیت‌های اختصاصی رو توی اون کلاس جدا تعریف کنیم. برای این کار، ارث‌بری به کار ما میاد.

برای اینکه یک کلاس رو توسعه بدیم، یک کلاس جدید می‌سازیم و از کلمه‌کلیدی extends به صورت زیر استفاده می‌کنیم:

class User { }

class WriterUser extends User {

}

توی این مثال، کلاس User به عنوان کلاس والد (Parent) و کلاس WriterUser به عنوان کلاس فرزند (Child) در نظر شناخته میشه. با این کار، کلاس فرزند همه ویژگی‌های کلاس والد رو در اختیار داره:

class User {
  login() {
    alert(&quotLogging in...&quot);
  }
}

class WriterUser extends User {

}

const david = new WriterUser();
 david.login(); // Logging in...

حالا می‌تونیم کلاس WriterUser رو به صورت اختصاصی توسعه بدیم:

class User {
  login() { }
  logout() { }
}

class WriterUser extends User {
  writePost() { }
  deletePost(id) { }
}

const david = new WriterUser();
david.login();
david.writePost();
david.logout();

با این کار کلاس User از ویژگی‌هایی که برای اون بی‌معنی هست خالی میشه و برنامهٔ ما معنادارتر و توسعهٔ اون راحت‌تر میشه ?

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


دسترسی به اعضای کلاس والد

با استفاده از کلمه‌کلیدی super توی یک کلاس می‌تونیم به اعضای کلاس والد دسترسی داشته باشیم:

class Parent {
  parentMethod() {
    alert(&quotI'm a method in the parent&quot);
  }
}

class Child extends Parent {
  childMethod() {
     super.parentMethod();
  }
}

const obj = new Child();
obj.childMethod(); // I'm a method in the parent

اگه هم کلاس والد و هم کلاس فرزند constructor دارن، کلاس فرزند توی constructor خودش، باید constructor والد رو صدا بزنه. این کار با صدا زدن تابعی به اسم ()super اتفاق میوفته:

class Parent {
  constructor() {
    alert(&quotI'm a method in the parent&quot);
  }
}

class Child extends Parent {
  constructor() {
     super();
  }
}

const obj = new Child(); // I'm a method in the parent

به این صورت این تابع می‌تونه یک ورودی رو به constructor والد پاس بده:

class User {
  constructor(name) {
    this.name = name;
  }
}

class WriterUser extends User {
  constructor(name) {
     super(name);
  }

  writePost() { }
  deletePost(id) { }
}

const david = new WriterUser(&quotDavid&quot);
alert(david.name); // David

اگه super رو صدا نزنیم خطا می‌گیریم. توی بعضی از زبان‌های برنامه‌نویسی ممکنه خطا نگیریم، اما این کار باعث میشه constructor والد اجرا نشه.

اما کدی که نوشتیم یک کار اضافی هست. چون توی constructor فرزند هیچ کار دیگه‌ای غیر از صدا زدن constructor والد انجام ندادیم. توی چنین شرایطی بهتره از نوشتن constructor فرزند صرف نظر کنیم:

class User {
  constructor(name) {
    this.name = name;
  }
}

class WriterUser extends User {
  writePost() { }
  deletePost(id) { }
}

const david = new WriterUser(&quotDavid&quot);
alert(david.name); // David

با ساخته شدن نمونه از کلاس فرزند، constructor والد به طور خودکار اجرا میشه.


فرزند و زیرفرزند

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

class A1 {
  constructor() {
    alert(&quotI'm A1&quot);
  }
}

class A2 extends A1 {
  constructor() {
    super();
    alert(&quotI'm A2&quot);
  }
}

class A3 extends A2 {
  constructor() {
    super();
    alert(&quotI'm A3&quot);
  }
}

new A3;



بررسی کردن نوع آبجکت

برای اینکه ببینیم یک آبجکت از نوع یک کلاس خاص هست یا نه، از عملگر instanceof استفاده می‌کنیم. این عملگر به ما true یا false برمی‌گردونه:

class User { }

const emily = new User();

alert(emily instanceof User); // true
alert(emily instanceof Array); // false




خب دوستان با کلاس‌ها و ویژگی‌های اونها توی ES6 جاوااسکریپت آشنا شدیم و دیدیم که با اونها ساختن آبجکت‌هایی که ویژگی‌های مشابه دارن چقدر راحت میشه!

Resources:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

https://dmitripavlutin.com/javascript-classes-complete-guide