آموزش جاوا اسکریپت – جلسه ۲۱ – کلاس و شی گرایی

در بخش بیستم از سری آموزشی جاوا اسکریپت، مدیریت کردن خطاهای احتمالی در جاوا اسکریپت را توسط try catch آموزش دادیم. در این مقاله می خواهیم مفهوم مهم شی گرایی و نحوه پیاده سازی آن را در زبان برنامه نویسی javascript آموزش دهیم. روش های مختلف تعریف کلاس (class) در جاوا اسکریپت، تعریف Getter-Setter، فیلدهای محاسباتی در کلاس، ارث بری (Inheritance) در کلاس، override کردن یک کلاس و… را با هم یاد میگیریم.
وقتی بخواهیم یک برنامه واقعی و عملی توسط javascript بنویسیم و از دنیای تئوری و آکادمیک دور شویم نیاز داریم چند شیء از یک نوع (مثلا کاربر یا محصول و…) ایجاد کنیم. همانطور که در فصل های قبل توضیح دادیم اینکار توسط دستور new function امکان پذیر است اما در جاوا اسکریپت مدرن مفهوم جدیدی اضافه شد بنام Class که شامل قابلیت های برنامه نویسی شیء گرا (Object Oriented) می باشد.
فرمت کلی Class در جاوا اسکریپت:
ساختار کلی کلاس بصورت زیر است:
class MyClass { // class methods constructor() { ... } method1() { ... } method2() { ... } method3() { ... } ... }
سپس برای استفاده از کلاس فوق و ساختن یک نمونه از آن دستور new MyClass را اجرا کنید تا یک نمونه از کلاس بالا با تمام متدها و propertyهایش برای شما ایجاد شود.
با اجرای دستور new متد سازنده یعنی constructor بطور اتوماتیک صدا زده می شود و می توانید در آن مقداردهی اولیه را انجام دهید.
بعنوان مثال:
class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } } // Usage: let user = new User("John"); user.sayHi();
با اجرای دستور new User(“John”); یک آبجکت از کلاس مذکور ایجاد می شود و متد سازنده با آرگومان ورودی name اجرا خواهد شد. سپس یک متد از کلاس بنام sayHi را فراخوانی کرده ایم.
نکته: بین متدهای کلاس هیچ ویرگولی وجود ندارد. برنامه نویس های تازه کار تصور می کنند باید بین متدهای یک کلاس ویرگول بگذارند اما اینکار غلط است و با خطای syntax مواجه خواهند شد. احتمالا افراد مبتدی تعریف متدهای کلاس را با تعریف آبجکت اشتباه می گیرند که باعث این خطا می شود.
تعریف کلاس (Class) در javascript:
در جاوا اسکریپت کلاس نوعی فانکشن است. به کد زیر دقت کنید:
class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } } // proof: User is a function alert(typeof User); // function
آن چیزی که class User {…} دقیقا انجام می دهند:
- یک تابع بنام User می سازد و نتیجه Class Declaration در آن قرار می گیرد. کد این تابع از متد سازنده گرفته می شود.
- متدهای کلاس (مانند sayHi) را در prototype ذخیره می کند.
با اجرای دستور new User یک شیء جدید از کلاس تعریف می شود و با فراخوانی متدهای آن، از prototype گرفته می شود. بنابراین آبجکت ایجاد شده به متدهای کلاس دسترسی دارد.
می توان گفت با اجرای دستور class User تصویر زیر را خواهیم داشت:
به قطعه کد زیر دقت کنید:
class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } } // class is a function alert(typeof User); // function // ...or, more precisely, the constructor method alert(User === User.prototype.constructor); // true // The methods are in User.prototype, e.g: alert(User.prototype.sayHi); // alert(this.name); // there are exactly two methods in the prototype alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
تعریف کلاس فقط برای زیبایی کد نیست!
برخی از افراد برنامه نویس تصور می کنند استفاده از دستور class صرفا برای syntactic sugar (کدی که فقط برای زیباسازی و خوانایی بیشتر تعریف می شود و قابلیت جدیدی به اسکریپت اضافه نمی کند). زیرا بدون تعریف Class هم می توانیم کد بالا را بنویسیم:
// rewriting class User in pure functions // 1. Create constructor function function User(name) { this.name = name; } // a function prototype has "constructor" property by default, // so we don't need to create it // 2. Add the method to prototype User.prototype.sayHi = function() { alert(this.name); }; // Usage: let user = new User("John"); user.sayHi();
نتیجه قطعه کد بالا با کدی که قبلا داشتیم یکسان است. در اینجا سوال پیش می آید که چرا اصلا باید از Class استفاده کنیم وقتی نتیجه یکسان است؟ در بخش زیر تفاوت های تعریف کلاس با حالتی که بدون کلاس کار می کنیم بیان شده است:
- اول اینکه تابعی که قبل از آن کلمه کلیدی Class تعریف می شود با تابع معمولی تفاوت دارد و برچسب زیر برای آن در نظر گرفته می شود:
[[FunctionKind]]:"classConstructor"
برخلاف تابع، برای فراخوانی یک کلاس (ایجاد نمونه جدید از کلاس) نیاز است از کلمه کلیدی new استفاده شود:
class User { constructor() {} } alert(typeof User); // function User(); // Error: Class constructor User cannot be invoked without 'new'
- تمام دستورات داخل کلاس بطور اتوماتیک بصورت use strict هستند.
- تمام متدهای یک کلاس non-enumerable هستند و در واقع flag مربوط به enumerable برای متدهای کلاس در prototype برابر false است.
- استفاده از Class منجر به فراهم شدن قابلیت های زیادی خواهد شد که در ادامه به آن می پردازیم.
تعریف کلاس بصورت Class Expression:
درست مانند توابع، کلاس ها هم می توانند بصورت Expression تعریف شوند. بصورت زیر:
let User = class { sayHi() { alert("Hello"); } };
همانند توابع، کلاس ها هم می توانند هنگام تعریف بصورت Expression نام داشته باشند. دقت کنید که این نام فقط در بلاک کلاس تعریف شده است و خارج از آن شناخته نشده است:
// "Named Class Expression" // (no such term in the spec, but that's similar to Named Function Expression) let User = class MyClass { sayHi() { alert(MyClass); // MyClass name is visible only inside the class } }; new User().sayHi(); // works, shows MyClass definition alert(MyClass); // error, MyClass name isn't visible outside of the class
حتی می توان کلاس موردنظر را بصورت داینامیک و بر حسب نیاز تعریف کرد:
function makeClass(phrase) { // declare a class and return it return class { sayHi() { alert(phrase); }; }; } // Create a new class let User = makeClass("Hello"); new User().sayHi(); // Hello
Getters/setters:
کلاس ها می توانند شمال متدهای get و set و نیز فیلدهای محاسباتی (computed property) باشند. در کد زیر روی user.name متدهای get و set را تنظیم کرده ایم:
class User { constructor(name) { // invokes the setter this.name = name; } get name() { return this._name; } set name(value) { if (value.length < 4) { alert("Name is too short."); return; } this._name = value; } } let user = new User("John"); alert(user.name); // John user = new User(""); // Name is too short.
در این مثال در متد set گفتیم که اگر تعداد کاراکترهای رشته ورودی از 4 کمتر باشد یک پیغام خطا چاپ کند و return کند وگرنه خود name را چاپ کند.
فیلدهای محاسباتی توسط براکت […]:
در کد زیر از دستور محاسباتی براکت برای رشته استفاده کرده ایم:
class User { ['say' + 'Hi']() { alert("Hello"); } } new User().sayHi();
در این مثال دو کلمه say و Hi با هم ترکیب شده اند و نام متد sayHi را شکل داده اند.
فیلدهای کلاس:
تا اینجا کلاس های ما فقط شامل متد بودند. اما کلاس می تواند علاوه بر متد شامل property نیز باشد. برای مثال می خواهیم فیلد name را به کلاس یوزر اضافه کنیم:
class User { name = "John"; sayHi() { alert(`Hello, ${this.name}!`); } } new User().sayHi(); // Hello, John!
معمولا پس از اتمام کار متد سازنده propertyهای کلاس پردازش می شوند. می توان برای propertyهای کلاس یک تابع را فراخوانی کنیم. مثلا:
class User { name = prompt("Name, please?", "John"); } let user = new User(); alert(user.name); // John
ایجاد متدهای اتصال ( bind ) توسط فیلدهای کلاس:
فانکشن های جاوا اسکریپت شامل یک this داینامیک هستند. مقدار آن به context محل فراخوانی بستگی دارد. بعنوان مثال خروجی کد زیر مقدار undefined خواهد بود:
class Button { constructor(value) { this.value = value; } click() { alert(this.value); } } let button = new Button("hello"); setTimeout(button.click, 1000); // undefined
مشکل کد فوق استفاده ناصحیح از this است.
به دو روش می توان مشکل فوق را حل کرد:
- پاس دادن یک wrapper-function بصورت setTimeout(() => button.click(), 1000)
- اتصال یا bind متد به آبجکت در متد سازنده به شکل زیر:
class Button { constructor(value) { this.value = value; this.click = this.click.bind(this); } click() { alert(this.value); } } let button = new Button("hello"); setTimeout(button.click, 1000); // hello
- استفاده از arrow function به شکل زیر:
class Button { constructor(value) { this.value = value; } click = () => { alert(this.value); } } let button = new Button("hello"); setTimeout(button.click, 1000); // hello
متد کلیک در کلاس فوق یعنی click = () => {…} بصورت arrow function تعریف شده است. خاصیت arrow function اینست که context را عوض نمی کند. یعنی this در آن به context جاری اشاره می کند. اما اگر از arrow استفاده نمی کردیم و متد click را بصورت تابع معمولی تعریف می کردیم، context عوض می شد و this.value در آن دیگر به کلاس اشاره نمی کرد و باعث بروز خطا می شد.
ارث بری کلاس ها (Class inheritance):
ارث بری کلاس ها روشی است برای extend کردن یک کلاس دیگر. بنابراین می توان ویژگی های جدیدی به کلاس فعلی اضافه کرد.
کلمه کلیدی extend:
فرض کنید یک کلاس Animal داریم:
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stands still.`); } } let animal = new Animal("My animal");
اگر بخواهیم آبجکت Animal و کلاس Animal را به تصویر بکشیم خواهیم داشت:
در ادامه فرض کنید می خواهیم یک کلاس Rabbit داشته باشیم. از آنجائیکه خرگوش هم نوعی حیوان است باید تمام خصوصیات animal را دارا باشد.
فرمت کلی extend کردن یک کلاس بصورت زیر است:
class Child extends Parent
اکنون بیایید کدی بنویسیم که کلاس خرگوش از کلاس حیوان ارث بری کند (آن را extend کند)
class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.hide(); // White Rabbit hides!
اکنون آبجکت rabbit هم به متدهای کلاس خودش دسترسی دارد مثلا rabbit.hide و هم به متدهای کلاس animal مثلا rabbit.run()
کلمه کلیدی extends بدین صورت کار می کند که متدها و صفات کلاس والد را برای کلاس فرزند نیز در نظر می گیرد. بنابراین اگر متدی روی کلاس فرزند صدا زده شود جاوا اسکریپت ابتدا در کلاس فرزند دنبال آن می گردد. اگر وجود نداشته باشد به سراغ کلاس والد (parent) می رود.
پس از صدا زدن متد run روی کلاس rabbit طبق تصویر بالا ابتدا از پایین شروع می شود و اگر در کلاس خرگوش متد run موجود باشد که استفاده می کند وگرنه به کلاس والد آن یعنی حیوان می رود و می بیند که run در آن وجود دارد.
نکته: هر عبارتی پس از extends می توان نوشت. لزومی ندارد که پس از کلمه کلیدی extends حتما class تعریف شود. می توان به شکل زیر هم عمل کرد:
function f(phrase) { return class { sayHi() { alert(phrase) } } } class User extends f("Hello") {} new User().sayHi(); // Hello
در این مثال کلاس User از نتیجه تابع f(“hello”) ارث بری می کند.
Override کردن یک متد:
همانطور که قبلا گفتیم هنگام فراخوانی یک متد ابتدا اولویت با متد کلاس فرزند است. در صورتیکه تعریف نشده باشد به سراغ کلاس والد می رود. اما اگر فرضا متد stop را که در کلاس animal وجود دارد در کلاس rabbit نیز تعریف کنیم در واقع آن را override کرده ایم. اولویت با همین متد کلاس فرزند خواهد بود و متد stop تعریف شده در کلاس والد نادیده گرفته می شود.
class Rabbit extends Animal { stop() { // ...now this will be used for rabbit.stop() // instead of stop() from class Animal } }
نکته: فرض کنید متد stop را در هر دو کلاس والد و فرزند داریم. همانطور که گفتیم اگر این متد را صدا بزنیم متد واقع در کلاس فرزند اجرا می شود. اما اگر بخواهیم در این حالت متد کلاس والد را فراخوانی کنیم چه باید کرد؟ راه حال این مسئله استفاده از کلمه کلیدی super است:
- دستور method(…) برای اجرای متد کلاس والد
- دستور super(…) برای اجرای متد سازنده کلاس والد
در مثال زیر می خواهیم خرگوش متد stop از والدش یعنی کلاس حیوان را override کند و یک متد جدید بنام hide داخل آن اجرا کند:
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stands still.`); } } class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } stop() { super.stop(); // call parent stop this.hide(); // and then hide } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // White Rabbit stands still. White rabbit hides!
در این حالت کلاس rabbit یک متد stop دارد که در آن ابتدا متد stop کلاس والدش را اجرا می کند و سپس متد hide خودش را اجرا می کند.
Override کردن متد سازنده:
در مورد متد سازنده قضیه کمی متفاوت خواهد بود. تا الان کلاس rabbit متد سازنده (constructor) نداشت. بر اساس Specification اگر کلاس B از کلاس A ارث بری کند و هیچ متد سازنده ای برای آن تعریف نشده باشد، یک constructor خالی به شکل زیر بطور اتوماتیک برای آن ایجاد می شود:
class Rabbit extends Animal { // generated for extending classes without own constructors constructor(...args) { super(...args); } }
همانطور که مشاهده می کنید اگر برای کلاس فرزند متد سازنده تعریف نشود، متد سازنده کلاس والد با تمام آرگومان ها برای آن تعریف خواهد شد.
اکنون بیایید برای کلاس rabbit متد سازنده مختص آن را تعریف کنیم. می خواهیم صفت earLength را بعلاوه name برای آن تعریف کنیم:
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { this.speed = 0; this.name = name; this.earLength = earLength; } // ... } // Doesn't work! let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
مشاهده می شود که در خط آخر ارور داریم و نمی توانیم یک آبجکت از کلاس Rabbit بسازیم.
اگر بخواهیم بطور خلاصه پاسخ این مشکل را بدهیم اینست که متدهای سازنده در کلاس هایی که توسط extends از یک کلاس دیگر ارث بری می کنند باید از super(…) استفاده کنند.
برای متد سازنده در کلاس خرگوش قبل از this باید از super استفاده کنیم.
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { super(name); this.earLength = earLength; } // ... } // now fine let rabbit = new Rabbit("White Rabbit", 10); alert(rabbit.name); // White Rabbit alert(rabbit.earLength); // 10
در این مقاله، مبحث با اهمیت شی گرایی (OOP: Object-Oriented) و پیاده سازی آن توسط کلاس (Class) را یاد گرفتیم. در مقاله بعدی یعنی بخش 22 از آموزش js درباره درخت DOM و نحوه پیمایش و سرچ عناصر در آن توضیح خواهیم داد.
دیدگاهتان را بنویسید