وقتی در این قسمت در مورد object-oriented js صحبت میکنیم، تمرکزمان روی ایجاد شیء ها است.
تعریف وراثت (Inheritance): یک شیء به ویژگی ها و متدهای شیء دیگر دسترسی داشته باشد. وقتی در مورد js صحبت میکنیم، ایده اصلی وراثت با زبان های برنامه نویسی دیگر متفاوت است.
منظور از وراثت classical آن وراثتی است که معروف است و در c# و جاوا وجود دارد. روشی است برای به اشتراک گذاشتن ویژگی ها و متدها. اینجا نمیگوییم که این نوع وراثت بد است اما اشتباه است فکر کنیم که فقط این نوع وراثت وجود دارد و معایبی ندارد. این روش خیلی سخاوتمندانه و باز است و تعداد خیلی زیادی ارتباط بین اشیاء ایجاد میشود. ممکن است خیلی سخت باشد که بفهمیم چگونه یکی روی دیگری تاثیر میگذارد. ممکن است چیزی را جایی تغییر دهیم و ببینیم چیز دیگری جایی دیگر تغییر کرده. در این نوع وراثت از کلمات friend و protected و private و interface استفاده میشود.
وراثت prototypal خیلی ساده تر، منعطف تر و extensible تر است و فهم آن خیلی راحت است. این نوع وراثت هم عالی و بدون نقص نیست اما باید بدانیم وقتی که در js در مورد وراثت صحبت میکنیم، تفاوت هایی با زبان های دیگر برنامه نویسی دارد. باید این موضوع توجه داشته باشیم که منظور از وراثت دسترسی یک شیء به ویژگی ها و متدهای شیء دیگری است. در js این کار با prototype انجام میشود.
مفهوم prototype: فرض کنید در حافظه یک شیء به اسم obj داریم. همان طور که میدانیم شیء ها میتوانند ویژگی و متد داشته باشند. مثلا prop1 را داریم که میتوانیم با obj.prop1 به آن دسترسی داشته باشیم. همچنین دیدیم که موتور js ویژگی ها و متدهای پنهانی هم میدهد. ویژگی ها یا متدهایی که معمولا به طور مستقیم با آنها کار نمیکنیم. در js همه اشیاء (از جمله تابع ها) یک ویژگی به اسم prototype دارند. این ویژگی یک رفرنس به یک شیء جدید به اسم proto است. خودش یک شیء است و میتوانیم از خودش هم استفاده کنیم. پس همین شیء proto هم ویژگی هایی دارد، مثلا prop2. اگر بگوییم obj.prop2 عملگر . در خود obj دنبال ویژگی prop2 میگردد و وقتی آن را پیدا نمیکند، سراغ prototype میرود و آنجا دنبال ویژگی prop2 میگردد. یعنی به prop2 به چشم ویژگی خودش نگاه میکند، ولی در واقع این ویژگی مربوط به prototype ش است.
این ویژگی proto خودش هم میتواند یک ویژگی proto داشته باشد و به شیء دیگری اشاره کند. هر شیء میتواند proto خودش را داشته باشد. ممکن است این proto دوم ویژگی prop3 را داشته باشد. پس وقتی بگوییم obj.prop3 چون داخل obj آن را پیدا نمیکند به proto آن میرود، در آنجا هم prop3 را پیدا نمیکند پس به proto بعدی میرود و وقتی پیدایش کرد آن را برمیگرداند. مثل این است که prop3 داخل خود obj اصلی باشد ولی در اصلی در prototype chain پیش رفته ایم تا پیدایش کرده ایم. نباید این را با scope chain اشتباه بگیریم. Scope chain در مورد جایی است که نگاه میکنیم تا به مقدار متغیر دسترسی داشته باشیم. اما prototype chain در مورد پیدا کردن ویژگی یا متد در رشته ای از اشیائی است که به خاطر ویژگی proto به هم متصل هستند. این مسئله از نظر ما پنهان است. یعنی نیازی نیست بگوییم obj.proto.proto.prop3 و میتوانیم فقط بگوییم obj.prop3. موتور js خودش در prototype chain جستجو میکند.
میتوانیم یک obj2 هم داشته باشیم که prototype ش همان prototype م obj باشد. پس میتوانیم داشته باشیم obj2.prop2 و همان ویژگی را برمیگرداند که برای obj.prop2 برگردانده بود. یعنی یک ویژگی به اشتراک گذاشته شده است.
var person = {
firstname: ‘Default’,
lastname: ‘Default’,
getFullName: function(){return this.firstname+’ ‘+this.lastname}
};
var john = {
firstname: ‘John’,
lastname: ‘Doe’
};
کاری که اینجا میخواهیم انجام دهیم برای یادگیری مهفوم prototype است و هیچ وقت نباید انجامش دهیم. بروزرهای مدرن راهی فراهم کرده اند تا به طور مستقیم به prototype دسترسی داشته باشیم. اما ما نباید از آن استفاده کنیم و تغییرش دهیم. چون در performance مشکلاتی پیش می آید و باعث کند شدن برنامه میشود.
// don’t do this EVER!
john.__proto__ = person;
console.log(john.getFullName());
همان طور که گفتیم همه اشیاء رفرنسی به شیء دیگری دارند که prototype نام دارد. در بسیاری از بروزرهای مدرن میتوانیم این کار را انجام دهیم. از __ قبل و بعد از proto استفاده شده تا مطمئن باشید هیچ وقت به طور اتفاقی از آن استفاده نکرده اید. اکنون john از person به ارث میبرد. یعنی اگر بخواهم در john به ویژگی یا متدی دسترسی داشته باشم که در john وجود ندارد، سراغ person میرود تا پیدایش کند. اگر در person هم نباشد سراغ proto مربوط به person میرود. خروجی کد بالا John Doe میشود. زمانی که تابع getFullName فراخوانی میشود، execution context ای که this را ایجاد میکند، میداند که در مورد چه شیئی صحبت میکنیم. پس this به person اشاره نمیکند، به john اشاره میکند، جایی که صدا زده شده. اگر در کنسول john.firstname را ببینیم مقدار John برگردانده میشود. چرا مقدار Default برگردانده نمیشود؟ چون اول خود john را چک میکند و اگر پیدایش کند دیگر سراغ prototype chain نمیرود. بنابراین firstname در john باعث میشود که firstname در prototype پنهان شود. این به دلیل این است که موتور js جستجو را از بالای زنجیره شروع میکند و هر موقع که پیدا شد جستجو متوقف میشود.
var jane = {
firstname: ‘Jane’
}
jane.__proto__ = person;
console.log(jane.getFullName());
پس proto هر دو شیء john و jane به یک جا از حافظه اشاره میکند. هر موقع که بخواهیم به متغیری دسترسی داشته باشیم prototype chain جستجو میشود. در این حالت this به jane اشاره میکند. پس در شیء jane دنبال firstname و lastname میگردد. jane فقط firstname را دارد. وقتی در jane دنبال lastname میگردد، آن را پیدا نمیکند و به prototype آن یعنی person نگاه میکند. خروجی Jane Default است.