درود دوستان. کلاسها (Classes) از ES6 به جاوااسکریپت اومدن تا جایگزینی برای توابع Constructor باشن که یک روش سنتی برای ساختن آبجکتها از یک طرح و الگوی اولیه به حساب میومد. این ویژگی کاربرد زیادی توی فریمورکها و کتابخونههایی مثل ریاکت و انگولار داره.
همونطور که میبینیم ۲ پراپرتی داریم و یک متد. حالا میخوایم چند آبجکت دیگه مشابه همین آبجکت داشته باشیم که فقط توی مقدار پراپرتیها با هم تفاوت دارن. شاید راهی که به ذهنمون برسه این باشه که هر آبجکتی رو دقیقاً مثل همون آبجکت بالا و به صورت دستی بسازیم:
const mario = {
name: "Mario",
age: 5,
run() {
alert(`${this.name} is running`);
}
}
const john = {
name: "John",
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 ساخته شدن و دارای ویژگیهایی مشترکی هستن. اما هنوز هیچ ویژگی (پراپرتی و متد) تعریف نکردیم که توی این نمونهها در دسترس باشن.
همه کلاسها میتونن یک متد اختصاصی داشته باشن به اسم constructor که به صورت زیر تعریف میشه:
class User {
constructor() {
// ...
}
}
این متد کاربرد جالبی داره: وقتی با کلمهکلیدی new میخوایم نمونه بسازیم، این متد به طور خودکار اجرا میشه:
class User {
constructor() {
alert("Bravo");
}
}
const mario = new User();
همونطور که میبینیم، برعکس متدهای دیگه لازم نیست این متد رو صدا بزنیم و خود به خود صدا زده شد. استفاده از اون برای زمانی خوبه که میخوایم نمونهها رو موقع ساختهشدن شخصیسازی کنیم. همونطور که از اسم اون بر میاد: Constructor یعنی سازنده.
شخصیسازی یک نمونه یعنی اینکه کاری کنیم پراپرتیهایی که توی نمونهها وجود دارن بتونن مقدارهای متفاوتی از بقیه نمونهها داشته باشن. مثلاً همه نمونهها یک پراپرتی داشته باشن به اسم name اما مقدار اون توی هر آبجکت فرق کنه. توی ادامه یاد میگیریم که چطوری نمونهها رو شخصیسازی کنیم.
برای اینکه بخوایم پراپرتیهایی داشته باشیم که توی همه نمونهها در دسترس باشن از متد constructor و this کمک میگیریم:
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const mario = new User("Mario", 5);
const emily = new User("Emily", 4);
alert(mario.name);
alert(mario.age);
alert(emily.name);
توی خط ۲ برای constructor دو پارامتر تعریف کردیم و که اونها رو موقع ساختن نمونه (خطهای ۸ و ۹) باید پاس بدیم. توی constructor با استفاده از کلمهکلیدی this تونستیم ۲ پراپرتی بسازیم و مقدار اونها رو برابر با ورودیهای این متد قرار بدیم. از حالا نمونههایی که ساخته میشن شامل این ۲ پراپرتی با مقدارهای اختصاصی خواهند بود و مثل خطهای آخر قابل دسترس هستن 👌
شاید واژه فیلد (Field) هم به گوشتون خورده باشه که با واژه پراپرتی بجای هم مورد استفاده قرار میگیرن. توی کد بالا و توی خط ۳ و ۴ ما در واقع فیلد تعریف کردیم. این فیلد با مقدارش زمانی که توی نمونهها قرار میگیرن، با عنوان پراپرتی شناخته میشن. پس پراپرتی چیزی هست که توی نمونهها و فیلد چیزی هست که توی کلاسها داریم. توی خطهای آخر کد بالا داریم پراپرتیها رو فراخونی میکنیم.
یک کلاس میتونه ویژگیهای یک کلاس دیگه رو به ارث ببره. این کار که به اصطلاح به اون گفته میشه ارثبری، برای زمانی خوبه که میخوایم یک کلاس موجود رو توسعه (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("Logging in...");
}
}
class WriterUser extends User {
}
const david = <<new WriterUser()>>;
>> david.login();
حالا میتونیم کلاس 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("I'm a method in the parent");
}
}
class Child extends Parent {
childMethod() {
>> super.parentMethod();
}
}
const obj = new Child();
obj.childMethod();
اگه هم کلاس والد و هم کلاس فرزند constructor دارن، کلاس فرزند توی constructor خودش، باید constructor والد رو صدا بزنه. این کار با صدا زدن تابعی به اسم super() اتفاق میوفته:
class Parent {
constructor() {
alert("I'm a method in the parent");
}
}
class Child extends Parent {
constructor() {
>> super();
}
}
const obj = new Child();
به این صورت این تابع میتونه یک ورودی رو به constructor والد پاس بده:
class User {
constructor(name) {
this.name = name;
}
}
class WriterUser extends User {
constructor(name) {
>> super(name);
}
writePost() { }
deletePost(id) { }
}
const david = new WriterUser("David");
alert(david.name);
اگه super رو صدا نزنیم خطا میگیریم. توی بعضی از زبانهای برنامهنویسی ممکنه خطا نگیریم، اما این کار باعث میشه constructor والد اجرا نشه.
اما کدی که نوشتیم یک کار اضافی هست. چون توی constructor فرزند هیچ کار دیگهای غیر از صدا زدن constructor والد انجام ندادیم. توی چنین شرایطی بهتره از نوشتن constructor فرزند صرف نظر کنیم:
class User {
constructor(name) {
this.name = name;
}
}
class WriterUser extends User {
writePost() { }
deletePost(id) { }
}
const david = new WriterUser("David");
alert(david.name);
با ساخته شدن نمونه از کلاس فرزند، constructor والد به طور خودکار اجرا میشه.