در برنامهنویسی، اغلب اوقات ما میخواهیم که چیزی را دریافت کنیم و آن را گسترش دهیم.
برای مثال، ما یک شیء user
همراه با ویژگیها و متدهای آن داریم و میخواهیم admin
و guest
را به عنوان نمونههایی از آن که تغییر کمی دارند بسازیم. ما میخواهیم چیزی را که در user
داریم را دوباره استفاده کنیم، نه اینکه متدهای آن را کپی/دوباره پیادهسازی کنیم، فقط یک شیء جدید را بر اساس آن بسازیم.
وراثت پروتوتایپی(prototypal inheritance) یک ویژگی زبان است که به این موضوع کمک میکند.
ویژگی [[Prototype]]
در جاوااسکریپت، شیءها یک ویژگی پنهانی [[Prototype]]
(دقیقا همانطور که در مشخصات زبان نامگذاری شده) دارند که یا null
است یا به شیء دیگر رجوع میکند. آن شیء «یک پروتوتایپ (prototype)» نامیده میشود:
زمانی که ما یک شیء را از object
میخوانیم و وجود ندارد، جاوااسکریپت به طور خودکار آن را از پروتوتایپ دریافت میکند. در برنامهنویسی، به این کار «وراثت پروتوتایپی» میگویند. و به زودی ما مثالهای زیادی از چنین وراثتی را خواهیم دید، درست مانند خصوصیتهای خفنتر زبان که بر اساس آن ساخته شدهاند.
ویژگی [[Prototype]]
درونی و پنهان است اما راههایی برای مقداردهی آن وجود دارد.
یکی از آن راهها استفاده از نام خاص __proto__
است، مثلا اینگونه:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // را تنظیم میکند rabbit.[[Prototype]] = animal
حالا اگر ما ویژگیای را از rabbit
بخوانیم و وجود نداشته باشد، جاوااسکریپت به طور خودکار آن را از animal
دریافت میکند.
برای مثال:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// پیدا کنیم rabbit حالا میتوانیم هر دو ویژگی را در
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
اینجا خط (*)
شیء animal
را به عنوان پروتوتایپ rabbit
تنظیم میکند.
سپس زمانی که alert
سعی میکند تا ویژگی rabbit.eats
(**)
را بخواند، درون rabbit
نیست پس جاوااسکریپت مرجع [[Prototype]]
را دنبال میکند و ویژگی را درون animal
پیدا میکند (از پایین به بالا نگاه کنید):
اینجا میتوانیم بگوییم که “animal
” پروتوتایپ rabbit
است یا “rabbit
” به صورت پروتوتایپی از animal
ارثبری کرده است.
بنابراین اگر animal
تعداد زیادی ویژگی و متد مفید داشته باشد، سپس آنها به طور خودکار درون rabbit
هم موجود میشوند. چنین ویژگیهایی را «موروث یا به ارثرسیده» میگویند.
اگر ما یک متد درون animal
داشته باشیم، میتواند با rabbit
هم فراخوانی شود:
let animal = {
eats: true,
walk() {
alert("جانور راه میرود");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// از پروتوتایپ برداشته شده است walk
rabbit.walk(); // جانور راه میرود
متد به طور خودکار از پروتوتایپ دریافت میشود، به این صورت:
زنجیرهی پروتوتایپ میتواند طولانیتر باشد:
let animal = {
eats: true,
walk() {
alert("جانور راه میرود");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// از رنجیرهی پروتوتایپ برداشته شده است walk
longEar.walk(); // جانور راه میرود
alert(longEar.jumps); // true (rabbit از)
حالا اگر ما چیزی را از longEar
بخوانیم و وجود نداشته باشد، جاوااسکریپت درون rabbit
و سپس درون animal
به دنبال آن میگردد.
فقط دو محدودیت وجود دارد:
- مرجعها نمیتوانند درون چرخه قرار بگیرند. اگر ما تلاش کنیم که
__proto__
را درون یک حلقه مقداردهی کنیم، جاوااسکریپت ارور ایجاد میکند. - مقدار
__proto__
میتواند شیء یاnull
باشد. انواع دیگر داده نادیده گرفته میشوند.
همچنین ممکن است واضح باشد اما باز هم: فقط یک [[Prototype]]
میتواند وجود داشته باشد. یک شیء نمیتواند از دو شیء دیگر ارثبری کند.
__proto__
یک getter/setter قدیمی برای [[Prototype]]
استاین یک اشتباه توسعهدهندگان تازهوارد است که تفاوت میان این دو را ندانند.
لطفا توجه کنید که __proto__
با ویژگی درونی [[Prototype]]
یکسان نیست. این ویژگی یک getter/setter برای [[Prototype]]
است. بعدا ما موقعیتهایی را خواهیم دید که این موضوع اهمیت دارد، اما چون فهم خود را از زبان جاوااسکریپت میسازیم، بیایید فقط این را در ذهن خود داشته باشیم.
ویژگی __proto__
کمی منسوخ شده است. بنا به دلایلی مربوط به گذشته هنوز وجود دارد، جاوااسکریپت مدرن پیشنهاد میکند که ما باید از تابعهای Object.getPrototypeOf/Object.setPrototypeOf
به جای آن دریافت/مقداردهی کردن پروتوتایپ استفاده کنیم. این تابعها را هم در آینده پوشش میدهیم.
بر اساس مشخصات زبان، __proto__
فقط باید توسط مرورگرها پشتیبانی شود. اگرچه در واقع تمام محیطها شامل سمت سرور از __proto__
پشتیبانی میکند، پس ما برای استفاده از آن به مشکلی بر نخواهیم خورد.
به دلیل اینکه نشان __proto__
از لحاظ درک کردن کمی بیشتر واضح است، در مثالها از آن استفاده میکنیم.
نوشتن از پروتوتایپ استفاده نمیکند
پروتوتایپ فقط برای خواندن ویژگیها استفاده میشود.
عملهای نوشتن/حذف کردن به صورت مستقیم با شیء کار میکنند.
در مثال پایین، ما متد walk
را در خود rabbit
مقداردهی میکنیم:
let animal = {
eats: true,
walk() {
/* استفاده نخواهد شد rabbit این متد توسط */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("خرگوش! بپر-بپر!");
};
rabbit.walk(); // !خرگوش! بپر-بپر
از این پس، فراخوانی rabbit.walk()
بدون اینکه از پروتوتایپ استفاده کند، بلافاصله متد را در شیء پیدا و آن را اجرا میکند:
ویژگیهای اکسسر استثنا هستند، مقداردهی توسط تابع setter انجام میشود. پس نوشتن در چنین ویژگیای در واقع با فراخوانی تابع یکسان است.
به همین دلیل admin.fullName
در کد پایین به درستی کار میکند:
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith (*)
// !فعال میشود setter
admin.fullName = "Alice Cooper"; // (**)
alert(admin.fullName); // Alice Cooper ،تغییر یافت admin وضعیت
alert(user.fullName); // John Smith ،حفظ شد user وضعیت
اینجا در خط (*)
ویژگی admin.fullName
در پروتوتایپ user
دارای یک getter است، پس این تابع فراخوانی میشود. و در خط (**)
ویژگی در پروتوتایپ دارای یک setter است پس این تابع فراخوانی میشود.
مقدار “this”
در مثال بالا ممکن است یک مثال جالب مطرح شود: مقدار this
درون set fullName(value)
چیست؟ ویژگیهای this.name
و this.surname
در کجا نوشته میشوند: درون user
یا admin
؟
جواب ساده است: پروتوتایپها بر روی this
هیچ تاثیری ندارند.
مهم نیست که متد کجا پیدا شده است: درون شیء یا پروتوتایپ آن. در فراخوانی یک متد، this
همیشه برابر با شیء قبل از نقطه است.
پس فراخوانی setter admin.fullName=
از admin
به عنوان this
استفاده میکند نه user
.
در واقع این یک موضوع بسیار مهم است چون ما ممکن است شیءای بزرگ با متدهایی زیاد و شیءهایی که از آن ارثبری میکنند داشته باشیم. و زمانی که شیءهای وارث از متدهای به ارثبردهشده استفاده میکنند، آنها فقط وضعیت خودشان را تغییر میدهند نه وضعیت شیء بزرگ را.
برای مثال، اینجا animal
نشان دهنده یک «حافظه متد» است و rabbit
از آن استفاده میکند.
فراخوانی rabbit.sleep()
ویژگی this.isSleeping
را در شیء rabbit
مقداردهی میکند:
// متدهایی دارد animal
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
// را تغییر میدهیم rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (چنین ویژگیای درون پروتوتایپ نیست)
تصویر حاصل:
اگر ما شیءهای دیگری هم داشتیم، مثل bird
، snake
و غیره، که از animal
ارثبری میکردند، آنها هم به متدهای animal
دسترسی پیدا میکردند. اما this
در هر فراخوانی متد، شیء متناظر خواهد بود، که در زمان فراخوانی ارزیابی میشود (قبل از نقطه)، نه animal
. پس زمانی که ما درون this
داده قرار میدهیم، درون این شیءها ذخیره میشود.
در نتیجه، متدها به اشتراک گذاشته میشوند، اما وضعیت شیء نه.
حلقه for…in
حلقه for..in
در ویژگیهای به ارثبردهشده هم حلقه میزند.
برای مثال:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// فقط کلیدهای خود شیء را برمیگرداند Object.keys
alert(Object.keys(rabbit)); // jumps
// هم در کلیدهای خود شیء و هم کلیدهای به ارثبردهشده حلقه میزنند for..in حلقههای
for(let prop in rabbit) alert(prop); // eats سپس ،jumps
اگر این چیزی نیست که ما میخواهیم و دوست داریم که شامل ویژگیهای به ارثبردهشده نشود، یک متد درونساخت obj.hasOwnProperty(key) وجود دارد: این متد اگر obj
ویژگی خودش (نه به ارثبردهشده) به نام key
را داشته باشد true
برمیگرداند.
پس میتواند ویژگیهای به ارثبردهشده را جداسازی کنیم (یا کاری دیگر با آنها کنیم):
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`برای ما: ${prop}`); // jumps :برای ما
} else {
alert(`به ارثبردهشده: ${prop}`); // eats :به ارثبردهشده
}
}
اینجا ما زنجیره ارثبری پایین را داریم: rabbit
از animal
ارثبری میکند که خود آن از Object.prototype
ارثبری میکند (چون animal
یک شیء لیترال {...}
است، پس این موضوع پیشفرض انجام میشود) و سپس null
در بالای آن:
در نظر داشته باشید که یک موضوع جالب وجود دارد. متد rabbit.hasOwnProperty
از کجا میآید؟ ما آن را تعریف نکردیم. با نگاه به زنجیره میتوانیم ببینیم که متد توسط Object.prototype.hasOwnProperty
فراهم شده. به عبارتی دیگر، به ارث برده شده است.
…اما اگر for..in
ویژگیهای به ارثبردهشده را لیست میکند، چرا hasOwnProperty
مثل eats
و jumps
که در حلقه for..in
ظاهر شدند، عمل نکرد؟
جواب ساده است: این ویژگی غیر قابل شمارش است. درست ماند تمام ویژگیهای دیگر Object.prototype
، این ویژگی پرچم enumerable: false
دارد. و for..in
فقط ویژگیهای قابل شمارش را لیست میکند. به همین دلیل این ویژگی و دیگر ویژگیهای Object.prototype
لیست نشدهاند.
تقریبا تمام متدهای دریافت کلید/مقدار دیگر، مانند Object.keys
، Object.values
و بقیه، ویژگیهای به ارثبردهشده را نادیده میگیرند.
آنها فقط روی خود شیء کارشان را انجام میدهند. ویژگیهای پروتوتایپ به حساب نمیآیند.
خلاصه
- در جاوااسکریپت، تمام شیءها یک ویژگی پنهان
[[Prototype]]
دارند که یا برابر با شیء است یاnull
. - ما میتوانیم از
obj.__proto__
برای دسترسی به آن استفاده کنیم (یک getter/setter قدیمی، راههای دیگری هم وجود دارد که به زودی پوشش داده میشوند). - شیءای که توسط
[[Prototype]]
به آن رجوع میشود «پروتوتایپ (prototype)» نام دارد. - اگر ما بخواهیم ویژگیای از
obj
را بخوانیم یا متدی از آن را فراخوانی کنیم و وجود نداشته باشد، سپس جاوااسکریپت سعی میکند که آن را درون پروتوتایپ پیدا کند. - عملیات نوشتن/حذف کردن به طور مستقیم روی شیء انجام میشوند، آنها از پروتوتایپ استفاده نمیکنند (با فرض اینکه یک ویژگی دادهای است، نه یک setter).
- اگر ما
obj.method()
را فراخوانی کنیم وmethod
از پروتوتایپ گرفته شود،this
هنوز هم بهobj
رجوع میکند. پس متدها همیشه با شیء کنونی کار میکنند حتی اگر آنها به ارثبردهشده باشند. - حلقه
for..in
هم درون ویژگیهای خود شیء و هم درون ویژگیهای به ارثبردهشده حلقه میزند. تمام متدهای گرفتن کلید/مقدار فقط روی خود شیء کارشان را انجام میدهند.