میخواهیم به طور عمیقتر در مورد ایجاد شیء صحبت کنیم. قبلا روش object literal syntax را برای ایجاد شیء دیدیم. اما روش های دیگری هم هستند، بخصوص وقتی میخواهیم برای شیء prototype هم تعیین کنیم. از روش های ساخت شیء function constructor و new است.
مثل الان که بین گوگل و اپل رقابت تکنولوژی هست، در گذشته هم بین تکنولوژی ها و زبان های برنامه نویسی مثل netscape و مایکروسافت و oracleSon رقابت وجود داشت. زبان js برای استفاده در بروزر نوشته شده بود. ابتدا به این منظور ساخته نشد ولی به تدریج با همین منظور استفاده میشد. اسم آن را جاوااسکریپت گذاشتند تا برنامه نویس های جاوا را جذب کنند، چون برنامه نویس های جاوا خیلی زیاد بودند و اگر برنامه نویس ها از زبانی استفاده نکنند، آن زبان از بین میرود. بنابراین شرکت اسم زبان را جاوااسکریپت گذاشت تا برنامه نویس های جاوا را جذب کند. مثل زمانی که مایکروسافت زبان بروزر را با اسم VBScript منتشر کرد. آن موقع VB خیلی محبوب بود. اسم جاوااسکریپت مثل جاوا هست و کمی شبیه آن است ولی در واقع هیچ شباهتی با آن ندارد. یکی از موارد بازاریابی که در جاوااسکریپت آورده شده خط کد زیر است:
var john = new Person();
برنامه نویسان جاوا به شکل بالا شیء ایجاد میکردند و از کلمه کلیدی new و چیزی به اسم کلاس استفاده میکردند. در جاوا کلاس یک شیء نیست، یک شیء را تعریف میکند. از کلمه new برای ایجاد شیء از آن کلاس استفاده میشود. در جاوااسکریپت کلاس نداریم. در نسخه بعدی جاوااسکریپت کلمه کلیدی class دارد به آن اضافه میشود، اما باز هم کلاس در جاوااسکریپت مثل کلاس در جاوا یا c# وجود ندارد. در جاوااسکریپت هم مثل جاوا با کلمه new میتوانیم شیء درست کنیم. اما در جاوااسکریپت لزومی به استفاده از new برای ایجاد شیء نداریم. وقتی برنامه نویسان جاوا به جاوااسکریپت نگاه میکنند میگویند جاوااسکریپت شبیه جاوا است پس از آن استفاده میکنیم. ولی در واقع این طور نیست. روشی که برای ایجاد شیء در جاوااسکریپت وجود دارد روش بدی نیست ولی مشکلاتی دارد. البته روش های جدیدی برای ایجاد شیء به جاوااسکریپت اضافه میشوند.
function Person() {
this.firstname = 'John';
this.lastname = 'Doe';
}
var john = new Person();
console.log(john);
پس ما فقط با نوشتن Person() آن را فراخوانی نکرده ایم. قبل از آن new گذاشته ایم. خروجی میشود:
Person { firsname:'John', lastname:'Doe' }
آنچه میخواهیم در این بخش درباره اش صحبت کنیم، روش های مختلف ایجاد کردن شیء است. نمیخواهیم شیءها یا نحوه عملکردشان را تغییر دهیم. فقط میخواهیم از ویژگی های زبان js برای ساخت اشیاء استفاده کنیم. برای ساخت شیء آن را ایجاد میکنیم و به آن ویژگی و متد میدهیم و prototype ش را تنظیم میکنیم. در بخش قبل prototype را با روش نادرستی تغییر دادیم، فقط برای اینکه عملکرد آن نشان داده شود.
کلمه کلیدی new برای شبیه کردن js به زبان برنامه نویسی رایج بود. این کلمه کلیدی در واقع یک عملگر است. وقتی کلمه new در کد آورده میشود، فورا یک شیء خالی ایجاد میشود. چیزی مثل کد زیر:
var a = { };
سپس تابع Person() فراخوانی میشود. با فراخوانی تابع execution context با متغیر this ایجاد میشود. زمانی که از کلمه کلیدی new استفاده میکنیم، آنچه this به آن اشاره میکند را تغییر میدهیم. متغیر this به آن شیء خالی اشاره میکند. پس وقتی داخل کد Person() م firstname و lastname را تعریف کرده ایم، یعنی این دو ویژگی را به شیء خالی اضافه کرده ایم. تا زمانی که تابع Person() مقداری را return نکند موتور js شیئی را که با عملگر new ایجاد شده برمیگرداند. پس یک شیء جدید ایجاد میشود، سپس تابع فراخوانی میشود و this به شیء خالی اشاره میکند و کارهایی که با متغیر this انجام میشوند مربوط به شیء خالی هستند و این شیء جدید برگردانده میشود. مثلا اگر در انتهای کد Person() خط کد زیر را اضافه کنیم:
console.log('This function is invoked');
خروجی میشود:
This function is invoked
Person { firstname:'John', lastname:'Doe' }
این نشان میدهد که تابع Person() فراخوانی شده است. اگر به ابتدای کد تابع Person() اضافه کنیم:
console.log(this);
خروجی Person { } یعنی یک شیء خالی خواهد بود. اما اگر کد تابع Person() را به صورت زیر تغییر دهیم:
function Person() {
this.firstname = 'John';
this.lastname = 'Doe';
return { greeting: 'i got in the way' };
}
var john = new Person();
console.log(john);
خروجی به صورت زیر میشود:
Object { greeting:'i got in the way' }
اما اگر داخل کد تابع return نداشته باشیم، موتور js میگوید که شما با عملگر new تابع را فراخوانی کرده اید، پس چیزی که برگردانده میشود یک شیء است که this به آن اشاره میکند، قبل از اینکه تابع شروع به اجرا کند و execution context ایجاد شود. پس توانستیم یک شیء را با استفاده از یک تابع بسازیم. به چنین تابعی که برای ساخت شیء از آن استفاده میکنیم function constructor میگوییم.
اگر بخواهیم شیء های بیشتری با ویژگی ها و متدهای یکسان ایجاد کنیم چه اتفاقی می افتد؟ مثلا به انتهای کد فوق اضافه کنیم:
var jane = new Person();
console.log(jane);
در خروجی میبینیم که مقدار john و jane هر دو به صورت زیر است:
Person { firstname:'John', lastname:'Doe' }
دو تا شیء مجزا ایجاد میشوند ولی مقدار firstname و lastname ثابت مانده. وقتی قبل از یک تابع عملگر new را میگذاریم کلمه کلیدی this را به یک شیء جدید اشاره میدهد و اگر در تابع چیزی return نشود، به جای برگرداندن undefined، آن شیء خالی برگردانده میشود. پس اگر بخواهیم که firstname و lastname هر آنچه میخواهیم باشد، از پارامتر استفاده میکنیم:
function Person(firstname, lastname) {
this.firsname = firstname;
this.lastname = lastname;
}
var john = new Person('John', 'Doe');
console.log(john);
var jane = new Person('Jane', 'Doe');
console.log(jane);
پس معمولا مثلا کد بالا در function constructor ها از پاس دادن مقادیر پیش فرض استفاده میشود. خروجی کد بالا:
Person { firsname:'John', lastname:'Doe' }
Person { firsname:'Jane', lastname:'Doe' }
تعریف function constructor: یک تابع معمولی که برای ساخت شیءها استفاده میشود. وقتی کلمه new را قبل از فراخوانی تابع قرار میدهیم، در آن تابع this به شیء جدید اشاره میکند و آن شیء به طور خودکار از تابع برگردانده میشود.
پس توانستیم متدها و ویژگی ها را با function constructor ایجاد کنیم. اما در مورد prototype چه؟
وقتی از function constructor استفاده میکنیم، خودش prototype را برای ما تنظیم میکند. مثلا در همان کد بخش قبل اگر در گوگل کروم در کنسول بنویسیم:
john.__proto__
خروجی Person { } خواهد بود، یک شیء خاص Person که خالی است. همان طور که قبلا گفته شد هر زمان که یک شیء تابع ایجاد میکنیم، یک سری ویژگی خاص (مثل NAME و CODE) خواهد داشت. یک ویژگی دیگر هم هست که همه تابع ها دارند و موقع function constructor باید از آن استفاده کنیم. همه تابع ها ویژگی prototype را دارند که شیء Empty است. اگر از تابع به عنوان یک function constructor استفاده کنیم، هیچ وقت از این ویژگی استفاده نمیشود. اما اگر از عملگر new موقع فراخوانی تابع استفاده کنیم، از این ویژگی استفاده میشود. بنابراین از ویژگی prototype فقط زمانی که از تابع به عنوان یک function constructor استفاده میکنیم استفاده میشود. وقتی از .prototype استفاده میکنیم، به نظر میرسد که داریم prototype شیء را تنظیم میکنیم، اما همان طور که قبلا گفته شد با .__proto__ میتوانیم این کار را انجام دهیم. ویژگی prototype در تابع prototype آن تابع نیست، بلکه prototype هر شیئی است که با این تابع ساخته میشود. همان طور که گفته شد همه تابع ها این ویژگی را دارند:
function Person(firstname, lastname) {
this.firsname = firstname;
this.lastname = lastname;
}
Person.prototype.getFullName = function() { return this.firstname+' '+this.lastname; };
var john = new Person('John', 'Doe');
console.log(john);
var jane = new Person('Jane', 'Doe');
console.log(jane);
قبلا گفته شده بود که اگر شیئی ویژگی یا متدی را نداشته باشد در prototype chain پیش میرود. وقتی از کلمه کلیدی new استفاده میکنیم، یک شیء خالی ایجاد میشود و prototype آن شیء خالی برابر با Person.prototype قرار داده میشود. بنابراین در مثال بالا john و jane هر دو به متد getFullName که در prototype شان است دسترسی دارند. خروجی کد بالا میشود:
Person { firstname:'John', lastame:'Doe', getFullName:function }
Person { firstname:'Jane', lastame:'Doe', getFullName:function }
همچنین میتوانیم بعدا چیزی را on the fly به prototype اضافه کنیم. مثلا در آخر کد بالا بنویسیم:
Person.prototype.getFormalFullName = function() { return this.lastname+', '+this.firstname; };
بعد از این کد میتوانیم از کد john.getFormalFullName() استفاده کنیم. بنابراین میتوانیم با استفاده از ویژگی prototye برای همه شیءهایی که با function constructor ساخته ایم، بعدا هم ویژگی هایی اضافه کنیم. یعنی اگر هزار شیء با استفاده از new از تابع Person ساخت باشیم، میتوانیم یک باره به همه آنها ویژگی یا متدی را اضافه کنیم. حتی بعد از اینکه این شیءها ساخته شده باشند. معمولا این طور است که ویژگی ها داخل function constructor ایجاد میشوند، چون معمولا مقادیر مختلفی دارند اما متدها در ویژگی prototype ایجاد میشوند. چرا متد getFullName را داخل تابع Person تعریف نکردیم؟ میتوانیم چنین کاری را انجام دهیم اما مشکلی ایجاد میشود: تابع ها در js شیء هستند. آنها حافظه ای را اشغال میکنند. هر چیزی که به آنها اضافه میکنیم فضایی میگیرد. بنابراین اگر مثلا متد getFullName را داخل آن بگذاریم، یعنی هر شیئی که با آن ساخته میشود یک کپی از getFullName برای خودش خواهد داشت. اما اگر این متد را به prototype اضافه کنیم، حتی اگر هزار شیء از تابع ساخته شوند فقط یک بار این تابع را خواهیم داشت. بنابراین برای کارآمدی بیشتر بهتر است که متدها را داخل prototype قرار دهیم. برای ویژگی ها این کار را نمیکنیم چون هر شیئی که ساخته میشود مقدار خودش را دارد.
روشی که در این بخش گفته شد یکی از روش های معتبر برای ایجاد شیء و تنظیم prototype آنها بود.
همان طور که گفته شد مثلا در کد new Person() تابع Person() فراخوانی میشود. کلمه new باعث برخی تغییرات میشود ولی در نهایت تابع Person() همچنان یک تابع است. مشکل وقتی ایجاد میشود که فراموش کنیم از کلمه کلیدی new قبل از آن استفاده کنیم:
var john = Person('John', 'Doe');
در این کد تابع Person() اجرا میشود. موتور js نمیداند که من میخواهم تابع را اجرا کنم یا با new آن را فراخوانی کنم. چون داخل کد تابع Person() مقداری return نمیشود، خودش مقدار undefined را برمیگرداند. بنابراین شیء john برابر با undefined خواهد شد. پس موقع استفاده از این روش باید حواسمان به استفاده صحیح از کلمه new باشد.
اسم هر تابعی را که میخواهیم یک function constructor باشد، با حرف بزرگ شروع میکنیم. اگر موقع اجرای برنامه با خطاهایی مواجه شدیم، میتوانیم کدمان را چک کنیم تا اگر جایی تابعی با حرف بزرگ به اشتباه بدون new فرخوانی شده، آن را اصلاح کنیم. این فقط یک قرارداد است. برخی برنامه ها هستند که میتوانند در این مورد به ما کمک کنند، به آنها linter گفته میشود. پیشنهاد میشود که اگر میخواهیم از function constructor استفاده کنیم، حتما حرف اول را بزرگ بنویسیم. برای ایجاد شیءها به غیر از روشی که گفته شد، روش های دیگری هم دارند به js اضافه میشوند و این روش کم کم (نه به طور کامل) کنار میرود.