در js هر چیزی که primitive نباشد یعنی تابع ها، آرایه ها، شیءها، همه یک prototype دارند، به غیر از یک چیز: شیء پایه در js.
var a = { };
var b = function() { };
var c = [ ];
اگر در گوگل کروم این کد را اجرا کنیم و در کنسول بنویسیم:
a.__proto__
خروجی Object{} را به ما برمیگرداند. این شیء پایه در js است. همیشه پایین ترین سطح زنجیره prototype است. پس اگر در کنسول بزنیم a.__proto__. یک سری متد به ما پیشنهاد میدهد، مثل toString. یعنی همه شیء ها به متد toString دسترسی دارند. اگر در کنسول بنویسیم:
b.__proto__
به ما خروجی function Empty(){} را برمیگرداند. این تابع prototype همه تابع ها است. اگر در همان کنسول بزنیم b.__proto__. متدهایی را پیشنهاد میدهد، مثل apply و bind و call. پس به همین دلیل است که همه تابع ها به این متدها دسترسی دارند. اگر در کنسول بنویسیم:
c.__proto__
به ما خروجی [ ] را میدهد. اگر در کنسول بزنیم c.__proto__. متدهای کاربردی پیشنهاد میدهد مثل indexOf و length و push. پس همه آرایه ها در js به چنین متدهایی دسترسی دارند. چون js خودش prototype را برای ما تنظیم میکند. اگر در کنسول بنویسیم:
b.__proto__.__proto__
c.__proto__.__proto__
برای هر دو خروجی Object{} را برمیگرداند. این شیء یک شیء built-in و هسته ای است و دیگر خودش prototype ندارد. پس به این روش است که میتوانیم به ویژگی ها یا متدهای شیءهای دیگر دسترسی داشته باشیم.
در کتابخانه ها و framework ها از ویژگی extend خیلی استفاده میشود. extend کردن به دلیل reflection امکان پذیر است.
تعریف reflection: یک شیء میتواند به خودش نگاه کند، ویژگی ها و متدهایش را لیست کند و آنها را تغییر دهد. پس شیء در js این توانایی را دارد که به ویژگی و متدهای خودش نگاه کند و میتواند از این ویژگی برای extend استفاده کند.
میخواهیم مثالی از reflection بزنیم و در آن از for in استفاده میکنیم. عبارت for in مثل foreach است. حلقه for in روی همه عضوهای شیء میچرخد. مثل این است که روی همه آیتم های یک آرایه حرکت کنیم. مقدار prop در هر تکرار برابر با همان عضو شیء خواهد بود. همان طور که گفته شد برای دسترسی به یک مقدار یک عضو میتوانیم به جای استفاده از . از [ ] استفاده کنیم تا به جای اسم عضو به آن متغیر بدهیم:
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function() { return this.firstname+' '+this.lastname; }
};
var john = {
firstname: 'John',
lastname: 'Doe'
};
//dont do this EVER!
john.__proto__ = person;
for (var prop in john){
console.log(prop + ': ' + john[prop]);
}
پس این حلقه روی همه ویژگی ها و متدهای john میچرخد. خروجی میشود:
firstname: John
lastname: Doe
getFullName: function() { return this.firsname+' '+this.lastname; }
همان طور که میدانیم getFullName در prototype قرار دارد. یعنی حلقه for in به همه ویژگی ها و متدها، نه فقط داخل خود شیء بلکه داخل prototype ش هم دسترسی دارد. اما اگر بخواهیم فقط موارد داخل خود شیء را بررسی کنیم چه کنیم؟
for (var prop in john){
if (john.hasOwnProperty('prop')) { console.log(prop + ': ' + john[prop]); }
}
متد hasOwnProperty در شیء پایه Object() وجود دارد، در john یا person نیست ولی میتوانیم در john به آن دسترسی داشته باشیم. یک رشته میگیرد و میگوید که آیا شیء واقعا ویژگی به اسم این رشته دارد یا نه. اگر این اسم داخل خود شیء نباشد false برمیگرداند. پس با این تغییر خروجی کد میشود:
firstname: John
lastname: Doe
پس توانستیم reflect کنیم در شیء john. یعنی به ویژگی هایش نگاه کنیم. توانستیم به metadata ویژگی های آن نگاه کنیم. میتوانیم آنها را لیست کنیم تا تغییر دهیم. با استفاده از این مفهوم ایده ای پیاده سازی شده که خیلی پرکاربرد است. در واقع ترکیبی است از inheritance و prototype. ولی در js م built-in نیست. بنابراین بسیاری از framework ها و کتابخانه ها خودشان آن را ساخته اند چون خیلی مفید است. میخواهیم از کتابخانه underscore استفاده کنیم. اگر در ادامه کد بالا داشته باشیم:
var jane = {
address: '111 Main St',
getFormalFullName: function() { return this.lastname+', '+this.firstname; }
};
var jim = {
getFirstName: function(){ return firstname; }
};
برای این دو شیء جدید prototype را تغییر ندادیم. آنچه میتوانیم مثلا با کتابخانه underscore انجام دهیم به صورت زیر است:
_.extend(john, jane, jim);
میخواهیم john را به شیءهای دیگری extend کنیم. این خط کد این شیءها را ترکیب میکند. همه ویژگی ها و متدهای jane و jim (ممکن است خیلی بیشتر از دو تا باشد) را میگیرد و آنها را به طور مستقیم به john اضافه میکند. میتوانستیم این کار را خودمان هم به طور دستی انجام دهیم. خروجی console.log(john); خواهد بود:
firstname: John
lastname: Doe
Object { firstnam:'John', lastname:'Doe', address:'111 Main St', getFormalFullName:function, getFirstName:function... }
یعنی firstname و lastname را از خودش دارد، address و getFormalFullName را از jane و getFirstName را از jim گرفته. همچنین prototype ش هم همان person است که در آن getFullName را داریم. پس این مسئله خیلی با prototype chain متفاوت است. این کار به صورت فیزیکی و واقعا ویژگی ها را داخل شیء john قرار میدهد. آنها را ترکیب کرده ایم. حال میخواهیم ببینیم چطور این کار انجام گرفته. پس سراغ underscore میرویم. در کد آن extend را سرچ میکنیم:
_.extend = createAssigner(_.allKeys);
پس باید دنبال تابع createAssigner بگردیم:
var createAssigner = function(keysFunc, undefinedOnly) {
return function(obj){
var length = arguments.length;
if (length<2 || obj==null) return obj;
for (var index=1; index<length; index++) {
var source=arguments[index], keys=keysFunc(source), l=keysLength;
for (var i=0; i<l; i++) {
var key = keys[i];
if (!indefinedOnly || obj[key]===void 0) obj[key] = source[key];
}
}
return obj;
};
};
تابع createAssigner تعداد keys میگیرد و یک تابع برمیگرداند. یعنی یک closure ایجاد میکند. پس در واقع وقتی extend را فراخوانی میکنیم، تابعی که اجرا میشود تابعی است که بعد از return اصلی نوشته شده. این تابعی که اجرا میشود به تابع های دیگر با closure دسترسی دارد. همان طور که میدانیم arguments همه ویژگی هایی هستند که پاس داده میشوند. اگر length کمتر از 2 بود فقط obj که به آن پاس داده ایم را برگردان. یعنی اگر در کدی که خودمان زده ایم، فقط بنویسیم:
_.extend(john);
مقدار length برابر با 1 است و خود john را برمیگرداند. چون چیزی به extend نداده ایم تا به john اضافه کند.
در حلقه for index هم length تعداد پارامترهایی که به extend پاس داده ایم را نشان میدهد. پس اگر مثلا تعداد آنها 2 باشد باید با index برابر با 1 شروع کنیم تا اولین پارامتر را skip کند (میدانیم که آرایه ها 0-based هستند). پس john را skip کرده و سراغ پارامتر بعدی میرود. میدانیم که میتوان در قسمت function(obj) پارامترهای دیگری را هم اضافه کنیم و نیازی نیست که حتما اینجا آنها را لیست کنیم، چون با کلمه کلیدی arguments در دسترس هستند. پس این حلقه روی پارامترهای jane و jim اجرا میشود.
خط کد source=arguments[index] ابتدا jane را برمیگرداند. keys در واقع همان اسم ویژگی ها است. پس اسم همه name/value ها را برای jane برمیدارد. خط کد l=keys.length تعداد ویژگی ها و متدهای jane را داخل l میریزد. در این مثال jane دو عضو دارد، پس مقدار l برابر با 2 میشود. سپس یک حلقه روی آنها حرکت میکند: هر ویژگی یا متد را میگیرد و دوباره چک میکند که حتما آنجا باشد (!undefinedOnly) و سپس obj ای که داشتیم (در این مثال john) را بروزرسانی میکند و برای آن ویژگی یا متدی با همان اسم obj[key] ایجاد میکند. پس اینجا reflection دارد انجام میشود. چون دارد به ویژگی و متدهایش نگاه میکند و دارد آنها را برای شیء john تنظیم میکند. پس حلقه اول روی شیءهایی که به عنوان source و پارامتر داده ایم میچرخد و حلقه دوم روی ویژگی ها و متدهای هر کدام از آنها میچرخد.
پس دیدیم که چقدر reflection بخصوص در extend مفید است. ویژگی extend خیلی استفاده میشود و باعث میشود برای فشرده سازی شیء ها همیشه به prototype chain نیاز نداشته باشیم. با مطالعه کد underscore و فهم آن میتوانیم خودمان چنین کدی را نوشته و از آن در کتابخانه شخصی خودمان استفاده کنیم. البته در نسخه جدید js چیزی به اسم extends وجود دارد ولی این ویژگی prototype را تنظیم میکند.