تعریف کلاس یا شی ECMAScript
استفاده از شیءهای پیشتعریف تنها بخشی از تواناییهای زبانهای برنامهنویسی مبتنی بر شیء است، واقعاً قدرت آن در توانایی ایجاد کلاسها و شیءهای اختصاصی خود است.
ECMAScript دارای بسیاری از روشهای ایجاد شیء یا کلاس است.
روش کارخانهای
روش اولیه
چون ویژگیهای شیء میتوانند در زمان ایجاد شیء به صورت دینامیک تعریف شوند، بسیاری از توسعهدهندگان در زمان معرفی اولیه JavaScript، کد مشابه زیر را نوشتهاند:
var oCar = new Object; oCar.color = "blue"; oCar.doors = 4; oCar.mpg = 25; oCar.showColor = function() { alert(this.color); };
در کد بالا، شیء car ایجاد میشود و سپس چندین ویژگی به آن اختصاص داده میشود: رنگ آن آبی است، دارای چهار در است و هر گالن بنزین میتواند 25 مایل را طی کند. آخرین ویژگی یک اشارهگر به توابع است، به این معنا که این ویژگی یک روش است. پس از اجرای این کد، میتوان از شیء car استفاده کرد.
اما اینجا یک مشکل وجود دارد که ممکن است نیاز به ایجاد چندین نمونه از car باشد.
راهحل: روش کارخانهای
برای حل این مشکل، توسعهدهندگان توابع کارخانهای ایجاد کردهاند که میتوانند اشیاء خاصی را ایجاد و بازگردانند.
مثلاً توابع createCar() میتوانند برای بستهبندی عملیات ایجاد شده برای شیء car استفاده شوند:
function createCar() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar(); var oCar2 = createCar();
در اینجا، تمام کدهای مثال اول در توابع createCar() قرار دارند. علاوه بر این، یک خط اضافی وجود دارد که شیء car را به عنوان ارزش توابع بازمیگرداند (oTempCar). فراخوانی این توابع، باعث ایجاد شیء جدید میشود و به آن تمام ویژگیهای مورد نیاز را اعطا میکند و شیء car را که در مثالهای قبلی توضیح داده شده است، کپی میکند. بنابراین، با این روش میتوان به راحتی دو نسخه از شیء car (oCar1 و oCar2) ایجاد کرد که ویژگیهای کامل مشابه دارند.
پارامترها را به توابع ارسال کنید
ما همچنین میتوانیم توابع createCar() را تغییر دهیم و به جای تعیین مقادیر پیشفرض برای ویژگیها، مقادیر پیشفرض مختلفی را به آن ارسال کنیم:
function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); // خروجی "red" oCar2.showColor(); // خروجی "blue"
با اضافه کردن پارامترها به توابع createCar()، میتوانید برای ویژگیهای car، رنگ، درها و mpg اشیاء ایجاد شده، مقادیر تعیین کنید. این باعث میشود دو شیء دارای ویژگیهای مشابه باشند، اما مقادیر مختلفی داشته باشند.
روشهای شیء را خارج از توابع کارخانه تعریف کنید
با وجود اینکه ECMAScript بیشتر رسمی میشود، اما روش ایجاد اشیاء نادیده گرفته شده است و تا کنون این استانداردسازی همچنان با مخالفت مواجه است. بخشی از آن به دلیل دلایل معنایی است (به نظر نمیرسد که به همان اندازه رسمی باشد که از عملگر new با توابع سازنده استفاده میشود) و بخشی نیز به دلیل دلایل عملکردی. دلایل عملکردی شامل روش ایجاد اشیاء به این روش است. در مثالهای قبلی، هر بار که توابع createCar() فراخوانی میشود، توابع جدید showColor() ایجاد میشود، به این معنا که هر شیء نسخهای از showColor() خود دارد. در حالی که در واقع هر شیء با یک توابع مشترک به اشتراک میگذارد.
بعضی توسعهدهندگان روشهای شیء را خارج از تابع کارخانهای تعریف میکنند و سپس از طریق ویژگی به این روش اشاره میکنند تا از این مسئله جلوگیری کنند:}
function showColor() { alert(this.color); } function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = showColor; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); // خروجی "red" oCar2.showColor(); // خروجی "blue"
در این کد بازنویسی شده، در بالا از تابع createCar()، تابع showColor() تعریف شده است. در داخل createCar()، یک اشارهگر به تابع showColor() که قبلاً وجود دارد، به شیء اختصاص داده میشود. از نظر عملکرد، این مسئله ایجاد تکراری تابع شیء را حل میکند؛ اما از نظر معنایی، این تابع به نظر نمیرسد که یک روش شیء باشد.
تمام این مسائل باعث ایجادتوسعهدهندگان تعریف میکنندظهور سازنده.
روش سازنده
ایجاد سازنده به همان سادگی که یک تابع کارخانهای ایجاد میشود. ابتدا باید نام کلاس را انتخاب کنید، که نام سازنده است. بر اساس عرف، اولین حرف این نام بزرگ نوشته میشود تا با نامهای متغیر که معمولاً با حرف کوچک شروع میشوند، جدا شوند. به جز این تفاوت، سازنده بسیار شبیه به تابع کارخانهای به نظر میرسد. به عنوان مثال زیر را در نظر بگیرید:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.showColor = function() { alert(this.color); }; } var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25);
در اینجا توضیح داده میشود که تفاوت کد بالا با روش کارخانهای چیست. ابتدا در داخل سازنده هیچ شیء ایجاد نمیشود، بلکه از کلید این استفاده میشود. هنگام استفاده از عملگر new برای سازنده، یک شیء جدید در ابتدای اجرای اولین خط کد ایجاد میشود و تنها با استفاده از این میتوان به شیء دسترسی پیدا کرد. سپس میتوان مستقیماً این ویژگیها را اختصاص داد، به طور پیشفرض این سازنده به عنوان بازگشت سازنده عمل میکند (لازم نیست که به صورت صریح از عملگر return استفاده شود).
حالا، با استفاده از عملگر new و نام کلاس Car، ایجاد اشیاء بیشتر شبیه به ایجاد اشیاء عمومی در ECMAScript خواهد بود.
شاید بپرسید، آیا این روش مشکلات مدیریت توابع مشابه روش قبلی دارد؟ بله.
مثل کارخانههای توابع، توابع ساختاری توابع را به طور مداوم تولید میکنند، و برای هر اشیاء مستقل نسخهای از توابع ایجاد میکنند. با این حال، مانند کارخانههای توابع، میتوان توابع ساختاری را با استفاده از توابع خارجی بازنویسی کرد، و این کار از نظر معنایی هیچ معنایی ندارد. این دقیقاً مزیت روش پیشوند است که در ادامه توضیح داده خواهد شد.
روش پیشوند
این روش از ویژگی prototype اشیاء استفاده میکند، که میتوان آن را به عنوان نمونهای از原型 برای ایجاد اشیاء جدید در نظر گرفت.
در اینجا، ابتدا با استفاده از توابع ساختاری خالی نام کلاس را تنظیم میکنیم. سپس تمام ویژگیها و روشها به طور مستقیم به ویژگی prototype اختصاص داده میشوند. مثال قبلی را با این کد بازنویسی میکنیم:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car();
در این کد، ابتدا توابع ساختاری تعریف میشوند (Car)، که هیچ کد دیگری در آن وجود ندارد. چندین خط بعد، با اضافه کردن ویژگیها به ویژگی prototype Car، ویژگیهای توابع ساختاری تعریف میشوند. با استفاده از new Car()، تمام ویژگیهای پیشوند به طور مستقیم به اشیاء ایجاد شده اختصاص داده میشوند، که به این معناست که تمام نمونههای Car به یک اشارهگر به توابع showColor() اشاره دارند. از نظر معنایی، تمام ویژگیها به نظر میرسد که به یک اشیاء تعلق دارند، بنابراین مشکلات دو روش قبلی حل شدهاند.
علاوه بر این، با استفاده از این روش، میتوان با استفاده از عملگر instanceof نوع اشیاء به آن اشاره شده توسط متغیرهای داده شده را بررسی کرد. بنابراین، کد زیر TRUE را خروجی خواهد داد:
alert(oCar1 instanceof Car); // خروجی "true"
مشکلات روش پیشوند
به نظر میرسد که روش پیشوند یک راه حل خوبی باشد. متاسفانه، اینطور نیست.
ابتدا، این توابع ساختاری هیچ پارامتری ندارند. با استفاده از روش پیشوند، نمیتوان با انتقال پارامترها به توابع ساختاری، مقادیر ابتدایی ویژگیها را تعیین کرد، زیرا ویژگی color، doors و mpg هر دو Car1 و Car2 برابر با "blue"، 4 و 25 هستند. این به این معناست که باید پس از ایجاد اشیاء، مقادیر پیشفرض ویژگیها را تغییر داد، که بسیار ناخوشایند است و هنوز تمام ماجرا تمام نشده است. مشکل واقعی زمانی رخ میدهد که ویژگیها به جای توابع به اشیاء اشاره میکنند. اشتراکگذاری توابع مشکلی ایجاد نمیکند، اما اشیاء به ندرت بین چندین نمونه اشتراکگذاری میشوند. بیایید به مثال زیر فکر کنیم:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.drivers = new Array("Mike","John"); Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car(); oCar1.drivers.push("Bill"); alert(oCar1.drivers); // نمایش "Mike,John,Bill" alert(oCar2.drivers); // نمایش "Mike,John,Bill"
در کد بالا، ویژگی drivers یک اشارهگر به شی Array است که شامل دو نام "Mike" و "John" میشود. زیرا drivers یک مقدار ارجاعی است، دو نمونه Car به همان آرایه اشاره میکنند. این بدان معناست که اضافه کردن مقدار "Bill" به oCar1.drivers، در oCar2.drivers نیز قابل مشاهده است. نمایش هر یک از این اشارهگرها، نتیجهای مشابه نمایش یک رشته "Mike,John,Bill" را نشان میدهد.
به دلیل اینکه در ایجاد شی این مسائل بسیاری وجود دارد، شما ممکن است فکر کنید که آیا روش معقولی برای ایجاد شی وجود دارد؟ پاسخ بله است، نیاز به استفاده از تابع سازنده و روش نمونهگیری است.
روش ترکیبی تابع سازنده/نمونهگیری
استفاده مشترک از تابع سازنده و روش نمونهگیری، میتوان مانند زبانهای برنامهنویسی دیگر شی ایجاد کرد. این مفهوم بسیار ساده است، یعنی استفاده از تابع سازنده برای تعریف تمامی ویژگیهای غیر تابع شی، و استفاده از روش نمونهگیری برای تعریف ویژگیهای تابعی (روشها). در نتیجه، تمامی توابع تنها یک بار ایجاد میشوند و هر شی دارای نمونهای از ویژگیهای شی خود است.
ما مثال قبلی را تغییر دادیم، کد زیر است:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); } Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25); oCar1.drivers.push("Bill"); alert(oCar1.drivers); // نمایش "Mike,John,Bill" alert(oCar2.drivers); // نمایش "Mike,John"
اکنون بیشتر شبیه ایجاد یک شی عمومی است. تمامی ویژگیهای غیر تابع در تابع سازنده ایجاد میشوند، به این معناست که میتوانیم با استفاده از پارامترهای تابع سازنده ارزشهای پیشفرض برای ویژگیها تعیین کنیم. زیرا تنها یک نمونه از تابع showColor() ایجاد میشود، بنابراین هیچ اتلاف حافظهای وجود ندارد. علاوه بر این، اضافه کردن مقدار "Bill" به آرایه drivers برای oCar1 تأثیری بر آرایه oCar2 ندارد، بنابراین هنگام نمایش مقادیر این آرایهها، oCar1.drivers "Mike,John,Bill" را نمایش میدهد و oCar2.drivers "Mike,John" را نمایش میدهد. زیرا از روش نمونهگیری استفاده شده است، بنابراین همچنان میتوان از عملگر instanceof برای تعیین نوع شی استفاده کرد.
این روش یکی از روشهای اصلی ECMAScript است که ویژگیهای روشهای دیگر را دارد، اما عوارض آنها را ندارد. با این حال، برخی توسعهدهندگان همچنان فکر میکنند که این روش به اندازه کافی کامل نیست.
روشهای پیشنویسی دینامیک
برای توسعهدهندگانی که به دیگر زبانها عادت دارند، استفاده از روشهای ترکیبی سازندهها/پروتوتایپها ممکن است خیلی هماهنگ نباشد. چرا که در تعریف کلاس، بیشتر زبانهای ابرپارادایمهای شیءگرا ویژگیها و روشها را از نظر بصری بستهبندی میکنند. به عنوان مثال، در نظر بگیرید کلاس Java زیر:
class Car { public String color = "blue"; public int doors = 4; public int mpg = 25; public Car(String color, int doors, int mpg) { this.color = color; this.doors = doors; this.mpg = mpg; } public void showColor() { System.out.println(color); } }
Java به خوبی تمام ویژگیها و روشهای Car کلاس را بستهبندی کرده است، بنابراین با دیدن این کد میتوان فهمید که چه کاری انجام میدهد، یک اطلاعات موجود در یک شیء را تعریف میکند. منتقدان روشهای ترکیبی سازندهها/پروتوتایپها معتقدند که روشی که در داخل سازندهها ویژگیها را جستجو میکنند و در خارج از آنها روشها را جستجو میکنند، غیرمنطقی است. بنابراین، آنها روشهای پیشنویسی دینامیک را طراحی کردهاند تا یک سبک کدنویسی دوستانهتر ارائه دهند.
ایدههای پایهای روشهای پیشنویسی دینامیک مشابه روشهای ترکیبی سازندهها/پروتوتایپها هستند، یعنی در داخل سازندهها غیرتابعها را تعریف میکنند و تابعها را از طریق ویژگیهای پروتوتایپ تعریف میکنند. تفاوت اصلی در مکان اختصاص داده شده به روشهای موجود در اشیاء است. در اینجا Car کلاس با استفاده از روشهای پیشنویسی دینامیک بازنویسی شده است:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); if (typeof Car._initialized == "undefined"}) { Car.prototype.showColor = function() { alert(this.color); }; Car._initialized = true; } }
این سازنده تا زمانی که typeof Car._initialized برابر با "undefined" نباشد تغییر نمیکند. این خط کد مهمترین بخش روش اولیهی پویا است. اگر این مقدار تعریف نشده باشد (و مقدار آن برابر با true باشد، مقدار typeof آن Boolean است)، سازنده به روش پروتوتایپ ادامه میدهد تا روشهای شیء را تعریف کند و Car._initialized را به true تنظیم کند. اگر این مقدار تعریف شده باشد (و مقدار آن true باشد)، این روش دیگر ایجاد نمیشود. به طور خلاصه، این روش از یک نشانه (Car._initialized) برای بررسی اینکه آیا روشهای پروتوتایپ به پروتوتایپها هرگز اختصاص داده شدهاند یا خیر، استفاده میکند. این روش فقط یک بار ایجاد و تنظیم میشود و توسعهدهندگان OOP سنتی از این کد بسیار خوشحال خواهند شد زیرا این کد شبیه به تعریف کلاسها در زبانهای دیگر به نظر میرسد.
روش کارخانهی ترکیبی
این روش معمولاً به عنوان یک راه حل جایگزین استفاده میشود که در شرایطی که نمیتوان از روش قبلی استفاده کرد استفاده میشود. هدف آن ایجاد یک سازندهی جعلی است که تنها یک نمونهی جدید از یک شیء را بازمیگرداند.
این کد بسیار شبیه به یک کارخانهی تولیدی است:
function Car() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; }
برخلاف روش کلاسیک، این روش از عملگر new استفاده میکند تا به نظر برسد که واقعاً یک سازنده است:
var car = new Car();
به دلیل اینکه در داخل سازندهی Car() عملگر new استفاده شده است، عملگر دوم new (که در خارج از سازنده قرار دارد) نادیده گرفته میشود و شیء ایجاد شده در داخل سازنده به متغیر car منتقل میشود.
این روش مشکلات مشابهی با روش کلاسیک در مدیریت روشهای شیء دارد. به شدت توصیه میشود: مگر اینکه واقعاً مجبور باشید، از این روش خودداری کنید.
استفاده از کدام روش
همانطور که قبلاً ذکر شد، روش استفاده شدهی بیشترین است روش ترکیبی از سازنده/پروتوتایپ. علاوه بر این، روشهای اولیهی پویا نیز بسیار محبوب هستند و از نظر عملکرد با روشهای سازنده/پروتوتایپ مشابه هستند. میتوان از هر یک از این روشها استفاده کرد. اما از روشهای سازنده/پروتوتایپ کلاسیک به تنهایی استفاده نکنید، زیرا این کار مشکلاتی را به کد شما وارد میکند.
مثال
یک نکته جالب در مورد اشیاء این است که چگونه از آنها برای حل مشکلات استفاده میشود. یکی از مشهورترین مشکلات ECMAScript در زمینه عملکرد پیوستن رشتههاست. مانند زبانهای دیگر، رشتههای ECMAScript غیرقابل تغییر هستند، یعنی مقدار آنها قابل تغییر نیست. به کد زیر توجه کنید:
var str = "hello "; str += "world";
در واقع، مراحل زیر را در پشت صحنه این کد انجام میدهد:
- رشتهای برای ذخیره "hello " ایجاد کنید.
- رشتهای برای ذخیره "world" ایجاد کنید.
- رشتهای برای ذخیره نتایج اتصال ایجاد کنید.
- محتوای فعلی str را به نتیجه کپی کنید.
- "world" را به نتیجه کپی کنید.
- str را بهروزرسانی کنید تا به نتیجه اشاره کند.
هر بار که اتصال رشتهها انجام میشود، مراحل 2 تا 6 اجرا میشوند، که باعث میشود این عملکرد بسیار منابع بر است. اگر این فرآیند را چند صد یا حتی چند هزار بار تکرار کنید، میتواند مشکلات عملکردی ایجاد کند. راهحل این است که از شیء آرایه برای ذخیره رشتهها استفاده کنید و سپس از روش join() (پارامتر خالی) برای ایجاد رشته نهایی استفاده کنید. تصور کنید که از کد زیر به جای کد قبلی استفاده کنید:
var arr = new Array(); arr[0] = "hello "; arr[1] = "world"; var str = arr.join(
بنابراین، مهم نیست که چند رشته به آرایه اضافه شوند، زیرا فقط در هنگام فراخوانی روش join() عملیات اتصال رخ میدهد. در این حالت، مراحل زیر را انجام دهید:
- رشتهای برای ذخیره نتایج ایجاد کنید
- هر رشتهای را به موقعیت مناسب در نتیجه کپی کنید
اگرچه این راهحل خوب است، اما روش بهتری وجود دارد. مشکل این است که این کد نمیتواند قصد خود را به خوبی نشان دهد. برای اینکه این بهتر قابل فهم باشد، میتوان این عملکرد را با استفاده از کلاس StringBuffer بستهبندی کرد:
function StringBuffer () { this._strings_ = new Array(); } StringBuffer.prototype.append = function(str) { this._strings_.push(str); }; StringBuffer.prototype.toString = function() { return this._strings_.join( };
این کد ابتدا باید به属性 strings توجه شود، که به معنای خصوصی است. این فقط دو روش دارد، یعنی روشهای append() و toString(). روش append() یک پارامتر دارد که این پارامتر را به آرایهی رشتهها اضافه میکند، روش toString() از روش join آرایهها استفاده میکند تا رشتهی واقعی که به هم متصل شدهاند را بازگرداند. برای اتصال یک گروه از رشتهها با استفاده از شیء StringBuffer، میتوان از کد زیر استفاده کرد:
var buffer = new StringBuffer(); buffer.append("hello "); buffer.append("world"); var result = buffer.toString();
میتوانید با استفاده از کد زیر عملکرد شیء StringBuffer و روشهای سنتی اتصال رشتهها را تست کنید:
var d1 = new Date(); var str = ""; for (var i=0; i < 10000; i++) { str += "text"; } var d2 = new Date(); document.write("Concatenation with plus: ") + (d2.getTime() - d1.getTime()) + " milliseconds"); var buffer = new StringBuffer(); d1 = new Date(); for (var i=0; i < 10000; i++) { buffer.append("text"); } var result = buffer.toString(); d2 = new Date(); document.write("<br />Concatenation with StringBuffer: ") + (d2.getTime() - d1.getTime()) + " milliseconds");
این کد دو تست برای اتصال رشتهها انجام میدهد، اولی با استفاده از علامت '+' و دومی با استفاده از کلاس StringBuffer. هر عمل اتصال 10000 رشته را انجام میدهد. تاریخهای d1 و d2 برای ارزیابی زمان لازم برای انجام عمل استفاده میشوند. توجه داشته باشید که اگر در هنگام ایجاد شیء Date هیچ پارامتری ارائه نشود، به آن تاریخ و زمان فعلی اختصاص داده میشود. برای محاسبه زمان لازم برای اتصال، میتوانید مقدار مگاژلوس (return value از روش getTime()) تاریخها را کم کنید. این روش معمولاً برای ارزیابی عملکرد JavaScript استفاده میشود. نتایج این تست میتواند به شما کمک کند تا تفاوت در عملکرد استفاده از کلاس StringBuffer و استفاده از علامت '+' را مقایسه کنید.