برنامه نویسی شیء گرا
در این بخش میخواهیم به بررسی یکسری از ویژگیها و نکات ریز برنامه نویسی شیء گرا در جاوا اسکریپت بپردازیم که یک برنامه نویس حرفهای جاوا اسکریپت حتما باید بر آنها واقف باشد تا بتواند کتابخانهها و Framework های موثرتر و بهینهتری را ایجاد کند. لازم به ذکر است که در این مجموعه مقالات، پیادهسازی اشیاء و شیوهی کد نویسی، بر اساس استاندارد ECMAScript 5 یا ES5
انجام خواهد شد. بنابراین از قابلیتهای جدیدی که در ES6 اضافه شدهاست، صحبت
نخواهیم کرد. پس از پایان این مجموعه مقالات و پس از آگاهی کامل از قابلیتهای جاوا
اسکریپت، در مجموعه مقالاتی به بررسی قابلیتهای جدید ES6 خواهیم پرداخت
که مرتبط به مقالات جاری است.
همانطور که قبلا اشاره شد، در زبانهای برنامه
نویسی شیء گرا، مفهومی به نام کلاس وجود دارد که ساختاری را جهت ایجاد اشیاء معرفی میکند و میتوانیم اشیاء مختلفی را از این کلاسها ایجاد نماییم. اما در جاوا
اسکریپت مفهوم کلاس وجود ندارد و فقط میتوانیم از اشیاء استفاده کنیم که نسبت به
زبانهای مبتنی بر کلاس متفاوت میباشد.
بر اساس تعریفی که از اشیاء در استاندارد ECMAScript
صورت گرفته است، هرشیء، شامل مجموعهای از ویژگیهاست، که هر یک از آنها میتواند حاوی یک مقدار پایه، شیء و یا تابع باشد. به عبارت دیگر هر شیء شامل آرایهای از
مقادیر است. هر ویژگی ( Property ) یا
تابع (که در برنامه نویسی شیء گرا متد نیز نامیده میشود) توسط نام خود شناسایی میشوند که به یک مقدار دادهای نگاشت یا Map شدهاند. به همین دلیل میتوان هر شیء
را به عنوان یک Hash Table
تصور کرد که دادهها را به صورت یک زوج کلید مقدار یا key-value pairs
نگهداری مینماید. در اینصورت نام ویژگیها و متدها به عنوان key و
مقدار آنها به عنوان value در
نظر گرفته میشوند.
مفهوم شیء
همانطور که قبلا اشاره شد، جهت تعریف اشیاء میتوان از دو روش استفاده نمود. در روش اول، ایجاد شیء با استفاده از شیء Object و در
روش دوم، با استفاده از Object
Literal Notation انجام خواهد شد. روش دوم جدیدتر و
بین برنامه نویسان جاوا اسکریپت محبوبتر است. مثال دیگری را جهت یادآوری در این
مورد ذکر میکنم:
var person = new Object();
person.firstName = "Meysam";
person.birth = new Date(1982, 11, 8);
person.getAge = function () {
var now = new Date();
return now.getFullYear() - this.birth.getFullYear();
}
alert(person.firstName + ": " + person.getAge()); // Meysam: 34
در مثال فوق، شیء person شامل دو ویژگی firstName و birth و
همچنین تابع getAge() میباشد. در تابع getAge() از
روی ویژگی birth یا
تاریخ تولد، سن شخص محاسبه شدهاست. همانطور که مشاهده میکنید، در داخل این تابع،
جهت دسترسی به ویژگی birth،
از
شیء this
استفاده نمودیم. this به
شیء ای اشاره میکند که تابع getAge() به
آن تعلق دارد و در اینجا به شیء person اشاره مینماید. اگر از
this
استفاده نکنید، برنامه خطا میدهد؛ زیرا قادر به شناسایی birth نمیباشد. مثال
فوق را میتوان با استفاده از Object
Literal Notation به صورت زیر نوشت: var person = {
firstName: "Meysam",
birth: new Date(1982, 11, 8),
getAge: function () {
var now = new Date();
return now.getFullYear() - this.birth.getFullYear();
}
};
alert(person.firstName + ": " + person.getAge()); // Meysam: 34
انواع Property ها
در ECMAScript 5 ، صفاتی برای Property ها معرفی شده است که از طریق Attribute های داخلی به Property ها اختصاص مییابد. این Attribute ها توسط
موتور جاوا اسکریپت بر روی Property ها پیاده سازی میشوند و به صورت مستقیم قابل دسترسی نمیباشند. در طی فرآیند آموزش
این مطالب، Attribute های داخلی را در [[]]
قرار میدهیم، مثل [[Enumarable]] ، تا
از سایر دستورات تفکیک شوند. به صورت کلی دو نوع ویژگی داریم که شامل Data Properties و Accessor Properties میباشند که به شرح آنها میپردازیم.
Data Properties
Data Property ها، 4 صفت یا Attribute را
توصیف میکنند که عبارتند از:
[[Configurable]]
مشخص میکند یک Property اجازه حذف،
تعریف مجدد و یا تغییر نوع را دارد یا خیر. بصورت پیش فرض، زمانی که یک شیء بصورت
مستقیم ساخته میشود، مقدار این ویژگی True میباشد.
[[Enumarable]]
مشخص میکند که آیا امکان پیمایش یک Property توسط حلقه for-in وجود دارد یا خیر. بصورت پیش فرض، زمانیکه یک شیء بصورت مستقیم ساخته میشود، مقدار این ویژگی True میباشد.
[[Writable]]
مشخص میکند که آیا مقدار یک Property قابل تغییر میباشد یا خیر. بصورت پیش فرض، زمانیکه یک شیء بصورت مستقیم ساخته میشود، مقدار این
ویژگی True میباشد.
[[Value]]
شامل مقدار واقعی یک Property و محل مقداردهی
یا برگرداندن مقدار Property ها میباشد. مقدار پیش فرض آن نیز undefined میباشد.
زمانیکه یک Property به صورت عادی
به یک شیء اضافه میشود، مانند مثالهای قبلی، سه Attribute اول
به true
تنظیم میشوند و [[Value]] با
مقدار اولیه Property
تنظیم میگردد. در این حالت آن Property ، قابل بروزرسانی و پیمایش میباشد. جهت تغییر ساختار یک Property و
تنظیم Attribute های آن، باید آن Property را
با استفاده از متد defineProperty()
تعریف نماییم . شکل
کلی تعریف Property با
استفاده از این متد به صورت زیر میباشد:
Object.defineProperty(obj, prop, descriptor)
آرگومان obj ، شیء ای است که Property مورد نظر باید به آن اضافه شود. آرگومان prop نام Property را
مشخص میکند که Attribute های آن باید تنظیم شوند. آرگومان descriptor یک شیء میباشد که Attribute های مورد نیاز را برای Property
تنظیم مینماید. شیء descriptor
شامل ویژگیهای configurable ، enumerable ، writable و value میباشد که میتوانند برای Property
تنظیم شوند. خروجی این متد شیء ای است که به عنوان آرگومان اول ارسال شدهاست. به
مثالهای زیر توجه کنید: var person = {};
Object.defineProperty(person, "name", {
writable: false,
value:"Meysam"
});
alert(person.name); // Meysam
person.name = "Arash";
alert(person.name); // Meysam
همانطور که در مثال فوق مشاهده میکنید، یک Property به
نام name به
شیء person
اضافه شدهاست که صفت writable آن
به false
تنظیم گردیدهاست. بنابراین امکان تغییر مقدار ویژگی name وجود ندارد و
با اینکه در دستور person.name = "Arash" ، ویژگی name را تغییر دادهایم، دستور alert
نهایی، مجددا خروجی Meysam را نمایش دادهاست. var person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Meysam"
});
alert(person.name); // Meysam
delete person.name;
alert(person.name); // Meysam
در مثال فوق، صفت configurable را
به false
تنظیم نمودهایم و همانطور که مشاهده میکنید امکان حذف ویژگی name توسط عملگر delete
وجود ندارد و دستور alert
نهایی مجددا خروجی Meysam را نمایش دادهاست. توجه داشته باشید که اگر شما بخواهید در خطوط بعدی کد،
مجددا صفت configurable را
به مقدار true
تغییر دهید، امکان پذیر نمیباشد. زیرا در تعریف فوق، صفت configurable را
به false
تنظیم نمودهاید و امکان بروزرسانی Attribute های ویژگی name را از آن گرفتهاید. در این حالت تنها Attribute ی
را که میتوانید تنظیم کنید، صفت writable میباشد. لازم به ذکر است که میتوانید متد defineProperty() را
چندین بار برای یک Property
فراخوانی نموده و در هر مرحله صفات متفاوتی را تنظیم و یا صفات قبلی را تغییر
دهید.
علاوه بر متد فوق، متد دیگری به نام defineProperties()
وجود دارد که میتوان چند Property را
بصورت همزمان تعریف و صفات آن را تنظیم نمود. شکل کلی این متد به صورت زیر است:
Object.defineProperties(obj, props)
آرگومان props یک شیء میباشد که ویژگیهای آن،
نام همان Property
هایی هستند که باید به obj
اضافه شوند. همچنین هر ویژگی خود یک شیء میباشد که میتوان صفات آن ویژگی را تنظیم
نمود. به مثال زیر توجه کنید:
var person = {};
Object.defineProperties(person, {
"name": {
configurable: false,
value: "Meysam"
},
"age": {
writable:false,
value:34
}
});
در مثال فوق، برای آرگومان props ، دو
ویژگی name و age را
تعریف نمودیم که این دو ویژگی به شیء person اضافه خواهند شد. همچنین ویژگیهای name و age خود
یک شیء میباشند که صفات مربوط به آنها تنظیم شده است. Accessor Properties
این صفات شامل توابع getter و setter میباشند که یک یا هر دوی آنها میتوانند برای یک Property تنظیم شوند.
زمانی که مقداری را از یک Property میخوانید، تابع getter
فراخوانی میشود و مقدار Property
مربوطه را بر میگرداند. این تابع میتواند قبل از برگرداندن مقدار، پردازش هایی را
بر روی آن Property
انجام داده و یک نتیجهی معتبر را برگرداند. زمانیکه Property را مقداردهی مینمایید، تابع setter
فراخوانی میشود و Property را
با مقدار جدید تنظیم مینماید. این تابع میتواند قبل از مقداردهی به Property ،
دادهی مورد نظر را اعتبارسنجی نماید تا از ورود مقادیر نامعتبر جلوگیری کند. Accessor Properties
شامل 2 صفت زیر میباشد:
[[Get]]
یک تابع میباشد و زمانی فراخوانی میگردد که
مقدار یک Property را
بخوانیم و مقدار پیش فرض آن undefined میباشد.
[[Set]]
یک تابع میباشد و زمانی فراخوانی میگردد که یک
Property را مقداردهی
نماییم و مقدار پیش فرض آن undefined میباشد. این تابع شامل یک آرگومان ورودی است که حاوی مقدار ارسالی به Property
است.
مثال زیر یک پیاده سازی ساده از شیء تاریخ شمسی میباشد که هنوز از لحاظ طراحی دارای نواقصی هست و در ادامه کارآیی و کد آن را بهبود میبخشیم.
var date = {
_year: 1,
_month: 1,
_day: 1,
isLeap: function () {
switch (this.year % 33) {
case 1: case 5: case 9: case 13:
case 17: case 22: case 26: case 30:
return true;
default:
return false;
}
}
};
Object.defineProperties(date, {
"year": {
"get": function () { return this._year; },
"set": function (newValue) {
if (newValue < 1 || newValue > 9999)
throw new Error("Year must be between 1 and 9999");
this._year = newValue;
}
},
"month": {
"get": function () { return this._month; },
"set": function (newValue) {
if (newValue < 1 || newValue > 12)
throw new Error("Month must be between 1 and 12");
this._month = newValue;
}
},
"day": {
"get": function () { return this._day; },
"set": function (newValue) {
if (newValue < 1 || newValue > 31)
throw new Error("Day must be between 1 and 31");
if (this.month === 12 && !this.isLeap() && newValue > 29)
throw new Error("Day must be between 1 and 29");
if (this.month > 6 && newValue > 30)
throw new Error("Day must be between 1 and 30");
this._day = newValue;
}
}
});
در مثال فوق، 3 ویژگی با نامهای _year ، _month و _day
تعریف شدهاند. پیشوند _
مشخص میکند که نباید به این ویژگی در خارج از شیء دسترسی داشته باشیم. البته
دسترسی را محدود نمیکند و برنامه نویس به راحتی میتواند به آن دسترسی داشته
باشد. در مباحث بعدی شیوهی صحیح پیاده سازی اینگونه Property ها را آموزش میدهیم. تابعی به نام isLeap() نیز
تعریف شده است که تشخیص میدهد سال موجود کبیسه هست یا خیر. با استفاده از تابع defineProperties() ، 3
ویژگی دیگر نیز به شیء date ، با
نامهای year ، month و day
اضافه نمودهایم که دارای Accessor های get و set میباشند. در بخش set
ورودیهای کاربران را بررسی و اعتبار سنجی نمودیم. در صورتی که ورودی نامعتبر باشد،
با استفاده از throw
خطایی را به صورت دستی ایجاد مینماییم که در console مربوط به Browser قابل مشاهده و یا با استفاده از try…catch
قابل دسترسی و مدیریت میباشد. دقت داشته باشید که لازم نیست حتما accessor های getter و setter با
هم برای یک Property تنظیم شوند و شما میتوانید فقط یکی از آنها را برای Property به
کار ببرید. اگر فقط تابع getter به
یک Property
اختصاص یابد، آن Property فقط
خواندنی میشود و امکان تغییر مقدار آن وجود ندارد. در این صورت هر دستوری که
اقدام به تغییر Property
نماید، بیتاثیر خواهد بود. همچنین اگر فقط تابع setter به یک Property
اختصاص یابد، آن Property فقط
نوشتنی میشود و امکان خواندن مقدار آن وجود ندارد. در این صورت هر دستوری که
اقدام به خواندن Property
نماید، مقدار undefined
برای آن برگردانده میشود.
نکتهی دیگری که باید به آن توجه کنید این است که اگر یک Property با
استفاده از متد defineProperty()
تعریف گردد، Attribute هایی
که مقداردهی نشدهاند، مثل [[Configurable]] ، [[Enumarable]] و [[Writable]] با false مقداردهی میگردند و [[Value]] ، [[Get]] و [[Set]]
مقدار undefined را
بر میگردانند. در مبحث بعدی، در مورد این نکته مثالی ارائه شده است.
خواندن Attribute های مربوط به یک Property
با استفاده از متد getOwnPropertyDescriptor() میتوان، Attribute های اختصاص داده شده به Property ها را خواند و از مقدار آنها مطلع
شد. این متد شامل 2 آرگومان میباشد، که آرگومان اول، شیء ای است که میخواهیم Attribute آن را بخوانیم و آرگومان دوم، نام Attribute میباشد. خروجی متد getOwnPropertyDescriptor() یک شیء از نوع PropertyDescriptor میباشد که ویژگیهای آن، همان Attribute
هایی هستند که برای یک Property
تنظیم شدهاند. به مثال زیر جهت خواندن Attribute های شیء تاریخ شمسی
توجه کنید:
var descriptor = Object.getOwnPropertyDescriptor(date, "_year");
alert(descriptor.value); // 1
alert(descriptor.configurable); // true
alert(typeof descriptor.get); // undefined
descriptor = Object.getOwnPropertyDescriptor(date, "year");
alert(descriptor.value); // undefined
alert(descriptor.configurable); // false
alert(typeof descriptor.get); // function
ویژگی _year به صورت عادی
تعریف شده است. بنابراین با توجه به نکاتی که قبلا ذکر شد، مقدار اختصاص داده شده
به این ویژگی، به صفت [[Value]]
تعلق گرفته است. همچنین سایر صفات این ویژگی به مانند [[Configurable]] ، با مقدار true
تنظیم شدهاند. Accessor های getter و setter نیز، که برای این ویژگی تنظیم نشده بودند، مقدار undefined بر میگردانند. ویژگی year با
استفاده از متد defineProperties()
تعریف شده است و چون Accessor های getter و setter به آن اختصاص یافتهاند، صفت [[Value]]،
مقدار undefined را
بر میگرداند و سایر Attribute ها به مانند [[Configurable]] که
تنظیم نشدهاند، مقدار false را
بر میگردانند. همچنین برای getter و setter نوع
function برگردانده شدهاست.