لازمه یادگیری زبان برنامه نویسی جاوا اسکریپت آشنایی خوب و عمیق با مفهوم شی در برنامه نویسی است. تسلط بر مفهوم «اشیا» (Objects) و مباحث پیرامون آن به عنوان یکی از اجزای اساسی زبانهای برنامه نویسی شی گرا محسوب میشود که جاوا اسکریپت نیز از این قاعده مستثنی نیست. در این مطلب از مجله تم آف پیرامون نمونههای اولیه یا Prototype در جاوا اسکریپت بحث شده است تا مفهوم اشیا در جاوا اسکریپت و نحوه تعامل با آنها را بهتر درک کنیم. پیش از پرداختن به بحث Prototype در جاوا اسکریپت، آشنایی با مفهوم اشیا یا شی در جاوا اسکریپت خالی از لطف نیست.
اشیا در جاوا اسکریپت
در جاوا اسکریپت، شی نوعی ساختار داده است که به کاربر امکان میدهد دادهها را در جفتهای «کلید-مقدار» (Key-Value) ذخیره و سازماندهی کند. شی یکی از انواع دادههای اساسی در جاوا اسکریپت به حساب میآید و مجموعهای از ویژگیها را در بر میگیرد که هر ویژگی از کلیدی (که نام ویژگی نیز نامیده میشود) و مقدارش تشکیل شده است.
اشیاء در جاوا اسکریپت پویا هستند، به این معنی که کاربر میتواند در هر زمان ویژگیها را تغییر دهد یا آنها را اضافه یا حذف کند. مقادیر ویژگیهای شی میتوانند از هر نوع دادهای باشند، از جمله اشیای دیگر، میتوان آرایهها، توابع و انواع اولیه مانند اعداد و «رشتهها» (استرینگ) را نام برد. مثال زیر مفهوم ایجاد شی در جاوا اسکریپت را بیان میکند.
const car = {
brand: "Tesla",
model: "Model S",
getInfo: function() {
console.log(`Brand: ${this.brand}, Model: ${this.model}`);
}
};
const bike = {
brand: "Honda",
model: "CBR 1000RR"
};
// Using bind to bind the car's getInfo method to the bike object
const boundGetInfo = car.getInfo.bind(bike);
boundGetInfo(); // Output: Brand: Honda, Model: CBR 1000RR
تصویر زیر نحوه ساخت شی در جاوا اسکریپت را نشان میدهد:
اگر فقط یک شی وجود داشته باشد، روند بالا ساده خواهد بود. اما در برخی از سناریوها، لازم است چندین چند شی مختلف ایجاد شود که این کار روند را کمی پیچیدهتر میکند. منطقیترین روش برای چنین سناریوهایی این است که منطق ایجاد شی در تابعی قرار بگیرد. با فراخوانی این تابع، میتوان در صورت نیاز شیئی جدید را ایجاد کرد. از این الگو به عنوان «نمونهسازی عملکردی یا کاربردی» (Functional Instantiation) یاد میکنند و تابع درگیر به عنوان «عملکرد سازنده» (Constructor Function) شناخته میشود زیرا اساساً شیئی جدید را میسازد.
نمونه سازی عملکردی
یکی از پیشنیازهای اصلی Prototype در جاوا اسکریپت درک مفهوم نمونهسازی عملکردی است.
برای درک مفهوم نمونه سازی عملکردی، فرض میشود که قطعه کد زیر را داریم. کدهای زیر حاوی تابعی برای تعریف حیوانات مختلف با ویژگی نام و انرژی است.
function Animal (name, energy) {
let animal = {}
animal.name = name
animal.energy = energy
animal.eat = function (amount) {
console.log(`${this.name} is eating.`)
this.energy += amount
}
animal.sleep = function (length) {
console.log(`${this.name} is sleeping.`)
this.energy += length
}
animal.play = function (length) {
console.log(`${this.name} is playing.`)
this.energy -= length
}
return animal
}
const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)
در کدهای بالا، هر زمان که حیوان جدیدی ایجاد شود، باید هر بار متدهای عمومی (eat و sleep ،play)
را بازسازی کرد. این روش کارآمد نیست و حافظه غیر ضروری را مصرف خواهد کرد. برای حل این مشکل، میتوان الگویی به نام «Functional Instantiation with Shared Methods» اتخاذ کرد. این الگو شامل انتقال متدهای عمومی به شیئی جداگانه و اجازه دادن به هر حیوانی برای ارجاع به آن شی است. با انجام این کار، میتوان از بازآفرینی متدها برای هر حیوان جلوگیری کرد و کدها را با حافظه بهینهتری نوشت.
نمونه سازی عملکردی با متدهای مشترک
به منظور پرداختن به مسئله زیادهروی در مصرف حافظه، الگوی نمونهسازی کاربردی با متدهای مشترک در کدهای زیر پیادهسازی شدهاند.
const animalMethods = {
eat(amount) {
console.log(`${this.name} is eating.`)
this.energy += amount
},
sleep(length) {
console.log(`${this.name} is sleeping.`)
this.energy += length
},
play(length) {
console.log(`${this.name} is playing.`)
this.energy -= length
}
}
function Animal (name, energy) {
let animal = {}
animal.name = name
animal.energy = energy
animal.eat = animalMethods.eat
animal.sleep = animalMethods.sleep
animal.play = animalMethods.play
return animal
}
const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)
در کدهای بالا، شیئی جداگانه به نام animalMethods
تعریف شده است که شامل متد عمومی (eat,sleep,play)
باشد. در تابع Animal
، یک شی حیوان جدید ایجاد و نام و خواص انرژی به آن داده شده است. به جای بازآفرینی متدها برای هر حیوان، اکنون با متدهای مشترک از شی animalMethods
کار انجام گرفته است. این رویکرد تضمین میکند که متدها تکراری نمیشوند و در نتیجه کدها با حافظه کارآمدتری ایجاد میشوند. با اتخاذ این الگو، مشکل اتلاف حافظه با موفقیت حل خواهد شد و اندازه اشیا کاهش داده میشود.
object.create در جاوا اسکریپت چیست؟
object.create نوعی متد جاوا اسکریپت داخلی است که به کاربر امکان میدهد شیئی جدید ایجاد و شی دیگری را به عنوان نمونه اولیه آن تنظیم کند. هنگامی که ویژگی خاصی در شی جدید یافت نمیشود، جاوا اسکریپت سعی میکند آن را در نمونه اولیه خود جستجو کند. به این رفتار، «تفویض» (Delegation) میگویند. بیایید این مفهوم را با مثالی توضیح دهیم.
فرض بر این است که شیئی والد به صورت زیر وجود دارد.
const parent = {
name: 'Stacey',
age: 35,
heritage: 'Irish'
}
میتوان شی child
با parent
به عنوان نمونه اولیه آن به صورت زیر ایجاد کرد.
const child = Object.create(parent);
child.name = 'Ryan';
child.age = 7;
حال، اگر کاربری بخواهد به ویژگیهای شی child
دسترسی پیدا کند، باید طبق قطعه کد زیر عمل شود.
console.log(child.name) // Ryan
console.log(child.age) // 7
console.log(child.heritage) // Irish
حتی اگر ویژگی heritage
مستقیماً روی شی child
تعریف نشده باشد، جاوا اسکریپت همچنان میتواند آن را در شی parent
پیدا کند که نمونه اولیه شی child
است. بنابراین چگونه میتوان از Object.create
برای ساده کردن کد Animal
قبلی استفاده کرد؟ با این اوصاف میتوان از Object.create
برای به اشتراک گذاشتن متدها بین نمونههای حیوانی مختلف استفاده کرد، نه اینکه این متدها به هر حیوان جداگانه اضافه شوند. این رویکرد را میتوان به عنوان نمونهسازی کاربردی یا عملکردی با متدهای مشترک به وسیله Object.create
نامید.
نمونه سازی کاربردی با متدهای مشترک و Object.create
متد Object.create
ابزاری مفید در جاوا اسکریپت به خساب میآید که به کاربر امکان میدهد شیئی را ایجاد کرده و جستجوهای ویژگی را به شیئی دیگر واگذار کند. این مفهوم به وسیله مثال مربوط به حیوانات به خوبی نشان داده شده است.
برای درک بهتر، بلوک کد زیر ارائه شده است که متدهای رایج برای حیوانات را تعریف میکند.
const animalMethods = {
eat(amount) {
console.log(`${this.name} is eating.`)
this.energy += amount
},
sleep(length) {
console.log(`${this.name} is sleeping.`)
this.energy += length
},
play(length) {
console.log(`${this.name} is playing.`)
this.energy -= length
}
}
سپس میتوان تابع Animal
را برای ایجاد اشیای حیوانی که به animalMethods
واگذار میشوند، به صورت زیر تعریف کرد:
function Animal (name, energy) {
let animal = Object.create(animalMethods)
animal.name = name
animal.energy = energy
return animal
}
const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)
leo.eat(10)
snoop.play(5)
در این مورد، وقتی leo.eat
فراخوانی میشود، جاوا اسکریپت ابتدا بررسی میکند که eat
در leo
وجود دارد یا خیر و از آنجایی که leo
این متد را ندارد، جاوا اسکریپت سپس به animalMethods
نگاه میکند. این به دلیل تفویض اختیاری اتفاق میافتد که از راه Object.create
ایجاد شده است.
با این حال، داشتن نوعی شی animalMethods
مجزا برای نگهداری متدهای مشترک، ممکن است چندان هم حرفهای به نظر نرسد. امکان دارد کاربری فکر کند این نوع به اشتراکگذاری متُد باید نوعی ویژگی باشد که در زبان تعبیه شده است. در واقع جاوا اسکریپت دارای چنین ویژگی است که به آن نمونه اولیه یا Prototype در جاوا اسکریپت میگویند. هر تابع در جاوا اسکریپت دارای یک ویژگی به نام نمونه اولیه یا Prototype است که به شی اشاره میکند. مثال زیر این مفهوم را بیان میکند:
function doThing () {}
console.log(doThing.prototype) // {}
اگر متدهای مشترک مستقیماً روی نمونه اولیه Animal
قرار داده شوند به این ترتیب، به جای تفویض اختیار به animalMethods
، به Animal.prototype
تفویض انجام خواهد شد. این الگو به عنوان Prototype در جاوا اسکریپت شناخته شده است.
Prototype در جاوا اسکریپت چیست؟
prototype جاوا اسکریپت ساز و کاری است که به وسیله آن، اشیا ویژگیهایی را از یکدیگر به ارث میبرند. به این ساز و کار، وراثت نمونه اولیه یا Prototype گفته میشود. هنگامی که تابعی در جاوا اسکریپت ایجاد میشود، موتور جاوا اسکریپت ویژگی Prototype را به تابع اضافه میکند. این ویژگی، نمونه اولیه نوعی شی به نام شی نمونه اولیه محسوب میشود که به طور پیشفرض دارای ویژگی سازنده است. ویژگی سازنده به تابعی اشاره میکند که در آن شی نمونه اولیه نوعی ویژگی است. میتوان به وسیله «FunctionName.prototype» به ویژگی نمونه اولیه تابع دسترسی پیدا کرد.
تمامی مباحث بالا، نوعی پیشنیاز برای Prototype در جاوا اسکریپت بودند. حال مفهوم Prototype در قالب مثالی ساده در این جا توضیح داده خواهد شد. فرض بر این است که تابعی به نام Animal
به صورت زیر وجود دارد:
function Animal (name, energy) {
let animal = Object.create(Animal.prototype)
animal.name = name
animal.energy = energy
return animal
}
Animal.prototype.eat = function (amount) {
console.log(`${this.name} is eating.`)
this.energy += amount
}
Animal.prototype.sleep = function (length) {
console.log(`${this.name} is sleeping.`)
this.energy += length
}
Animal.prototype.play = function (length) {
console.log(`${this.name} is playing.`)
this.energy -= length
}
const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)
leo.eat(10)
snoop.play(5)
درک عمیق کدهای بالا تا حدود زیادی راه را برای یادگیری مفهوم Prototype در جاوا اسکریپت آسان میکند. هر تابع جاوا اسکریپت، حاوی نوعی ویژگی Prototype اولیه است. این ویژگی متدهای به اشتراک گذاشته شده را در تمام نمونههای یک تابع فعال میکند و این در حالی خواهد بود که عملکرد یکسان باقی میماند و دیگر نیازی به مدیریت شیئی جداگانه برای متدها وجود نخواهد داشت. در عوض، از شیئی ساخته شده در خود تابع Animal
، یعنی Animal.prototype
استفاده میشود. این روش کدنویسی به کاهش پیچیدگی، بهبود کارایی و قابلیت نگهداری کدها کمک میکند.
کاربرد Prototype در جاوا اسکریپت چیست؟
هر زمان که تابع جاوا اسکریپت ایجاد میشوند، جاوا اسکریپت نوعی ویژگی Prototype به آن تابع اضافه میکند. Prototype در واقع نوعی شی است که میتواند متغیرها و متدهای جدیدی را به شی موجود اضافه کند. این یعنی Prototype نوعی کلاس پایه برای همه اشیا است و به کاربر کمک میکند تا از وراثت در برنامه نویسی استفاده کند.
عمیق شدن در Prototype در جاوا اسکریپت
تا اینجا از مبحث Prototype در جاوا اسکریپت موارد زیر پوشش داده شدند:
- نحوه ساخت تابع جدید
- اضافه کردن متدهایی به Prototype در جاوا اسکریپت
- استفاده از Object.create
برای تفویض جستجوهای ناموفق به نمونه اولیه تابع
ممکن است کاربری فکر کند که این عملیات باید سادهتر یا یکپارچهتر باشد. در جاوا اسکریپت، کلمه کلیدی New
این کار را انجام میدهد و فرآیند را سادهتر میکند. مثال زیر در این رابطه مهم است:
function Animal (name, energy) {
let animal = Object.create(Animal.prototype)
animal.name = name
animal.energy = energy
return animal
}
هنگامی که تابعی با استفاده از New
فراخوانی میشود، ایجاد و بازگشت شی به طور ضمنی انجام شده و شی ایجاد شده، this
نامیده میشود. با فرض اینکه سازنده Animal
با New
فراخوانی شود، میتوان آن را به صورت زیر بازنویسی کرد:
function Animal (name, energy) {
// const this = Object.create(Animal.prototype)
this.name = name
this.energy = energy
// return this
}
const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)
پس از حذف نظراتی که عملیات «Under tThe Hood» را توصیف میکنند، کد به صورت زیر خواهد بود.
function Animal (name, energy) {
this.name = name
this.energy = energy
}
Animal.prototype.eat = function (amount) {
console.log(`${this.name} is eating.`)
this.energy += amount
}
Animal.prototype.sleep = function (length) {
console.log(`${this.name} is sleeping.`)
this.energy += length
}
Animal.prototype.play = function (length) {
console.log(`${this.name} is playing.`)
this.energy -= length
}
const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)
دلیل ایجاد شی this
استفاده New
برای فراخوانی تابع سازنده است. بدون New
، نه شی this
ایجاد میشود و نه به طور ضمنی بازگردانده خواهد شد. این موضوع در ادامه نشان داده شده است.
function Animal (name, energy) {
this.name = name
this.energy = energy
}
const leo = Animal('Leo', 7)
console.log(leo) // undefined
اگر کاربر با زبانهای برنامه نویسی دیگر آشنا باشد، ممکن است متوجه شود که در قطعه کد بالا نوعی نسخه تقریبی از کلاس در برنامه نویسی ایجاد شده است. کلاس به کاربر امکان میدهد نوعی طرح اولیه برای شی تعریف کند و همچنین به کاربر امکان میدهد نمونههایی از کلاس را با ویژگیها و متدهای مشخص شده در طرح ایجاد کند. این اساساً همان چیزی است که با عملکرد سازنده Animal
به دست آمد.
جنبه مثبت این روش این است که جاوا اسکریپت در حال تکامل است و کمیته «TC-39» به طور مداوم آن را بهبود و گسترش میدهد. اگر چه نسخه اصلی جاوا اسکریپت از کلاسها پشتیبانی نمیکرد، اما این بدان معنا نیست که نمیتوان آنها را در مشخصات رسمی گنجاند. کمیته TC-39 دقیقاً این کار را در سال «۲۰۱۵» (١٣٩٣ شمسی) با انتشار «EcmaScript6» انجام داد که شامل کلاسها و کلمه کلیدی class
بود. عملکرد سازنده Animal
در کدهای بالا با «سینتکس» (Syntax) کلاس به صورت زیر خواهد بود:
class Animal {
constructor(name, energy) {
this.name = name
this.energy = energy
}
eat(amount) {
console.log(`${this.name} is eating.`)
this.energy += amount
}
sleep(length) {
console.log(`${this.name} is sleeping.`)
this.energy += length
}
play(length) {
console.log(`${this.name} is playing.`)
this.energy -= length
}
}
const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)
متدهای آرایه در جاوا اسکریپت
در بخش بالا به طور گسترده، به اشتراک گذاشتن متدها در بین نمونههای کلاس و Prototype در جاوا اسکریپت بحث شد، بهترین روش قرار دادن این متدها در نمونه اولیه کلاس (یا تابع) است. این الگو زمانی که کاربر با کلاس Array
سر و کار داشته باشد، مورد نیاز خواهد بود.
سینتکس ایجاد آرایه در جاوا اسکریپت به صورت زیر است:
const friends = []
سینتکس زیر هم راهی راحتتر برای ایجاد نمونه جدید از کلاس Array
است.
const friendsWithSugar = []
const friendsWithoutSugar = new Array()
چیزی که ممکن است ذهن کاربر را درگیر کند، این است چگونه هر نمونه آرایه همه متدهای آرایه در جاوا اسکریپت مانند spliceو slice،pop
را دریافت میکند. این کار به این دلیل امکانپذیر است که تمام متدها در Array.prototype
قرار دارند. هنگامی که نمونه آرایه جدیدی با استفاده از کلمه کلیدی New تولید میشود، نوعی بازگشت به Array.prototype
برای جستجوهای ناموفق ایجاد خواهد شد. در مطلبی جداگانه در «مجله تم آف» به بحث متدهای آرایه در جاوا اسکریپت به صورت کامل پرداخته شده است.
میتوان تمام متدهای آرایه را با ورود به سیستم Array.prototype
به صورت زیر مشاهد کرد.
console.log(Array.prototype)
/*
concat: ƒn concat()
constructor: ƒn Array()
copyWithin: ƒn copyWithin()
entries: ƒn entries()
every: ƒn every()
fill: ƒn fill()
filter: ƒn filter()
find: ƒn find()
findIndex: ƒn findIndex()
forEach: ƒn forEach()
includes: ƒn includes()
indexOf: ƒn indexOf()
join: ƒn join()
keys: ƒn keys()
lastIndexOf: ƒn lastIndexOf()
length: 0n
map: ƒn map()
pop: ƒn pop()
push: ƒn push()
reduce: ƒn reduce()
reduceRight: ƒn reduceRight()
reverse: ƒn reverse()
shift: ƒn shift()
slice: ƒn slice()
some: ƒn some()
sort: ƒn sort()
splice: ƒn splice()
toLocaleString: ƒn toLocaleString()
toString: ƒn toString()
unshift: ƒn unshift()
values: ƒn values()
*/
این اصل در مورد اشیا نیز صدق میکند. هر شی برای جستجوهای ناموفق به Object.prototype
واگذار میشود، به همین دلیل است که همه اشیا، متدهایی مانند toString
و hasOwnProperty
دارند.
متدهای استاتیک
تا اینجا، ما یاد گرفتیم که چگونه متدها را میتوان در بین نمونههای کلاس به اشتراک گذاشت، اما گاهی اوقات ممکن است به روشی نیاز پیدا کنیم که به کلاس مربوط است، اما نیازی به اشتراکگذاری در بین نمونهها وجود نداشته باشد. برای مثال، فرض میکنیم متدی به نام nextToEat
وجود دارد که آرایهای از نمونههای Animal
را میگیرد و تعیین میکند که کدام یک نیاز به تغذیه دارد.
در اینجا، کدهای تابع nextToEat
آورده شده است:
function nextToEat(animals) {
const sortedByLeastEnergy = animals.sort((a,b) => {
return a.energy - b.energy;
});
return sortedByLeastEnergy[0].name;
}
منطقی نیست که nextToEat
را در Animal.prototype
قرار دهیم، زیرا متدی نیست که بین همه نمونهها به اشتراک گذاشته شود. در عوض، میتوان آن را تابعی سودمند در نظر گرفت. میتوان nextToEat
را در همان محدوده کلاس Animal
قرار داد و در صورت لزوم از آن استفاده کرد. مانند کدهای زیر:
class Animal {
constructor(name, energy) {
this.name = name;
this.energy = energy;
}
eat(amount) {
console.log(`${this.name} is eating.`);
this.energy += amount;
}
sleep(length) {
console.log(`${this.name} is sleeping.`);
this.energy += length;
}
play(length) {
console.log(`${this.name} is playing.`);
this.energy -= length;
}
}
function nextToEat (animals) {
const sortedByLeastEnergy = animals.sort((a,b) => {
return a.energy - b.energy;
})
return sortedByLeastEnergy[0].name;
}
const leo = new Animal('Leo', 7);
const snoop = new Animal('Snoop', 10);
console.log(nextToEat([leo, snoop])); // Leo
روش بالا روشی مفید است، اما راهحل ظریفتری نیز برای این هدف وجود دارد. اگر متدی به کلاسی مرتبط باشد، اما نیازی به توزیع در بین نمونهها وجود نداشته باشد، میتوان آن را به عنوان نوعی ویژگی ثابت کلاس اضافه کرد. نحوه انجام این کار به صورت زیر است:
class Animal {
constructor(name, energy) {
this.name = name;
this.energy = energy;
}
eat(amount) {
console.log(`${this.name} is eating.`);
this.energy += amount;
}
sleep(length) {
console.log(`${this.name} is sleeping.`);
this.energy += length;
}
play(length) {
console.log(`${this.name} is playing.`);
this.energy -= length;
}
static nextToEat(animals) {
const sortedByLeastEnergy = animals.sort((a,b) => {
return a.energy - b.energy;
})
return sortedByLeastEnergy[0].name;
}
}
const leo = new Animal('Leo', 7);
const snoop = new Animal('Snoop', 10);
console.log(Animal.nextToEat([leo, snoop])); // Leo
از آنجایی که nextToEat
اکنون نوعی ویژگی ثابت کلاس است، به خود کلاس Animal
تعلق دارد (نه نمونه اولیه آن) و میتوان آن را با استفاده از Animal.nextToEat
فراخوانی کرد. در نهایت، بیایید همان رفتار را با استفاده از سینتکس «ES5» تکرار کنیم. کلمه کلیدی static
در مثال قبلی متد را مستقیماً به کلاس اضافه کرد. با «ES5»، میتوانیم به صورت دستی متد را به شی تابع اضافه کنیم:
function Animal(name, energy) {
this.name = name;
this.energy = energy;
}
Animal.prototype.eat = function(amount) {
console.log(`${this.name} is eating.`);
this.energy += amount;
};
Animal.prototype.sleep = function(length) {
console.log(`${this.name} is sleeping.`);
this.energy += length;
};
Animal.prototype.play = function(length) {
console.log(`${this.name} is playing.`);
this.energy -= length;
};
Animal.nextToEat = function(animals) {
const sortedByLeastEnergy = animals.sort((a,b) => {
return a.energy - b.energy;
});
return sortedByLeastEnergy[0].name;
};
const leo = new Animal('Leo', 7);
const snoop = new Animal('Snoop', 10);
console.log(Animal.nextToEat([leo, snoop])); // Leo
این رویکرد میتواند هنگام برخورد با متدهایی که به کلاس مربوط میشوند، به جای نمونههای فردی، خواناتر باشد و سازماندهی و وضوح کدها را بیشتر بهبود بخشد.
دریافت نمونه اولیه شی
صرف نظر از روشی که برای ایجاد یک شی استفاده میشود، میتوان نمونه اولیه شی یا Prototype در جاوا اسکریپت را با متد Object.getPrototypeOf
به دست آورد.
مثال زیر برای درک این مفهوم مهم است:
function Animal(name, energy) {
this.name = name;
this.energy = energy;
}
Animal.prototype.eat = function(amount) {
console.log(`${this.name} is eating.`);
this.energy += amount;
}
Animal.prototype.sleep = function(length) {
console.log(`${this.name} is sleeping.`);
this.energy += length;
}
Animal.prototype.play = function(length) {
console.log(`${this.name} is playing.`);
this.energy -= length;
}
const leo = new Animal('Leo', 7);
const prototype = Object.getPrototypeOf(leo);
console.log(prototype); // {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}
console.log(prototype === Animal.prototype); // true
۲ بینش مهم در کدهای فوق وجود دارد. اول اینکه شی نمونه اولیه، شامل چهار متُد «سازنده» ( constructor
)، «خوردن» ( eat
)، «خوابیدن» ( sleep
) و «بازی» ( play
) است. این کار منطقی به حساب میآید، زیرا وقتی getPrototypeOf
با مثال leo
فراخوانی میشود، نمونه اولیه نمونهای بازخواهد گشت که همه متدها در آن ذخیره میشوند. به طور پیش فرض، جاوا اسکریپت نوعی ویژگی سازنده را به نمونه اولیه اختصاص میدهد که به تابع یا کلاس اصلی که نمونه را ایجاد کرده است، پیوند داده خواهد شد. این بدان معنی است که نمونهها میتوانند به وسیله instance.constructor
به سازنده خود دسترسی داشته باشند.
همچنین در کد فوق Object.getPrototypeOf(leo) === Animal.prototype
به درستی ارزیابی میشود و تأیید میکند که getPrototypeOf
به کاربر اجازه میدهد نمونه اولیه خود نمونه را بررسی کند که باید مشابه Animal.prototype
باشد.
function Animal(name, energy) {
this.name = name;
this.energy = energy;
}
const leo = new Animal('Leo', 7);
console.log(leo.constructor); // Logs the constructor function
این رفتار به بحث قبلی در Object.create
گره خورده است. وقتی leo.constructor
فراخوانی میشود، leo
خاصیت سازنده ندارد. بنابراین، جستجو را به Animal.prototype
واگذار میکند که دارای ویژگی سازنده است. ممکن است کاربران با __proto__
برای دسترسی به نمونه اولیه یک نمونه برخورد کرده باشند، این روش منسوخ شده و همانطور که در مثالها نشان داده شده است، از Object.getPrototypeOf(instance)
استفاده میشود.
تعیین وجود ویژگی در Prototype در جاوا اسکریپت
در شرایط خاص، تشخیص اینکه آیا ویژگی در خود نمونه قرار دارد یا در نمونه اولیه واگذار شده، ضروری خواهد بود. فرض بر این است که کاربری میخواهد شی leo
مثالهای قبل را پیمایش و تمام کلیدها و مقادیر آن را ثبت کند.
با استفاده از حلقه for…in
، این کار به صورت زیر انجام خواهد شد.
function Animal(name, energy) {
this.name = name;
this.energy = energy;
}
Animal.prototype.eat = function(amount) {
console.log(`${this.name} is eating.`);
this.energy += amount;
}
Animal.prototype.sleep = function(length) {
console.log(`${this.name} is sleeping.`);
this.energy += length;
}
Animal.prototype.play = function(length) {
console.log(`${this.name} is playing.`);
this.energy -= length;
}
const leo = new Animal('Leo', 7);
for (let key in leo) {
console.log(`Key: ${key}. Value: ${leo[key]}`);
}
کاربر انتظار دارد خروجی زیر را ببیند.
Key: name. Value: Leo Key: energy. Value: 7
با این حال خروجی کدهای بالا به صورت زیر است.
Key: name. Value: Leo Key: energy. Value: 7 Key: eat. Value: function... Key: sleep. Value: function... Key: play. Value: function...
این اتفاق به این دلیل رخ میدهد که حلقه for…in
تمام ویژگیهای شمارشپذیر شی و نمونه اولیه آن را پیمایش میکند. به طور پیشفرض، هر ویژگی اضافه شده به prototype در جاوا اسکریپت نوعی تابع قابل پیمایش است. بنابراین، نه تنها name
و energy
، بلکه متدهای روی نمونه اولیه مانند splice و slice ،pop
نیز قابل پیمایش هستند. برای جلوگیری از این امر، به مکانیزمی نیاز است که فقط ویژگیهای خودِ شی leo
را به استثنای موارد موجود در نمونه اولیه ثبت کند. اینجا است که متد hasOwnProperty
وارد عمل میشود.
hasOwnProperty نوعی متد داخلی به حساب میآید که مقداری بولی را برمیگرداند و نشان میدهد که آیا شی دارای ویژگی مشخص شده متعلق به خود (و نه در نمونه اولیه) هست یا خیر. با این روش میتوان حلقه را به صورت زیر اصلاح کرد.
const leo = new Animal('Leo', 7);
for (let key in leo) {
if (leo.hasOwnProperty(key)) {
console.log(`Key: ${key}. Value: ${leo[key]}`);
}
}
اکنون، فقط ویژگیهایی قابل رویت هستند که روی خود شی leo
موجودند.
Key: name. Value: Leo Key: energy. Value: 7
کدهای زیر به درک بهتر مفهوم hasOwnProperty
کمک میکند.
const leo = new Animal('Leo', 7);
console.log(leo.hasOwnProperty('name')); // true
console.log(leo.hasOwnProperty('energy')); // true
console.log(leo.hasOwnProperty('eat')); // false
console.log(leo.hasOwnProperty('sleep')); // false
console.log(leo.hasOwnProperty('play')); // false
کدهای فوق نشان میدهند که hasOwnProperty
فقط برای ویژگیهای تعریفشده در خودِ نمونه name
و energy
مقدار true
برمیگرداند، نه برای آنهایی که در نمونه اولیه مانند splice و slice ،pop
تعریف شدهاند.
بررسی نمونه های کلاس برای اشیا
گاهی اوقات، ممکن است لازم باشد بررسی کنیم که آیا شی نمونهای از کلاسی خاص است یا خیر، پس در چنین مواردی میتوان از عملگر instanceof
استفاده کرد.
سینتکس استفاده از این عملگر به صورت زیر است.
object instanceof Class
اگر شی، نمونهای از Class
باشد، عبارت فوق، مقدار true
و اگر نمونهای از آن نباشد، false
را برمیگرداند. در ادامه نمونهی Animal
برای این هدف دوباره بررسی میشود.
function Animal(name, energy) {
this.name = name;
this.energy = energy;
}
function User() {}
const leo = new Animal('Leo', 7);
console.log(leo instanceof Animal); // true
console.log(leo instanceof User); // false
در کدهای فوق، leo instanceof Animal
مقدار true
را برمیگرداند، زیرا leo
نمونهای از Animal
است. برعکس، نمونه leo instanceof User
مقدار false
را بازمیگرداند، زیرا leo
نمونهای از User
نیست.
عملگر instanceof
با بررسی این مورد کار میکند که آیا constructor.prototype
در هر نقطه از زنجیره prototype در جاوا اسکریپت وجود دارد یا خیر، به عنوان نمونه، در مثال بالا، نمونه leo
از Animal
مقدار true
را برمیگرداند، زیرا Object.getPrototypeOf(leo)
برابر Animal.prototype
است. برعکس، leo instanceof User
مقدار false
را بازمیگرداند، زیرا Object.getPrototypeOf(leo)
برابر User.prototype
نیست.
ایجاد توابع سازنده آگنوستیک جدید
آیا میتوان اشتباه کدهای زیر را تشخیص داد؟
function Animal(name, energy) {
this.name = name
this.energy = energy
}
const leo = Animal('Leo', 7)
حتی توسعهدهندگان باتجربه جاوا اسکریپت ممکن است با این مثال برخورد کنند. مسئله اینجا است که وقتی تابع سازنده Animal
با استفاده از الگوی شبه کلاسیکی فراخوانی میشود، باید از کلمه کلیدی New
استفاده کند. اگر این کار انجام نشود، کلمه کلیدی This در جاوا اسکریپت وجود نخواهد داشت و به طور ضمنی برگردانده نخواهد شد. برای یادآوری، هنگامی که از کلمه کلیدی New
در تابعی استفاده شود، این مراحلی که به صورت نظرات در کدهای زیر درج شدهاند، رخ میدهد.
function Animal(name, energy) {
// const this = Object.create(Animal.prototype)
this.name = name
this.energy = energy
// return this
}
در کدهای بالا، آیا میتوان تضمین کرد که سازنده Animal
همیشه با کلمه کلیدی جدید فراخوانی میشود؟
میتوان این کار را با استفاده از عملگر instanceof
انجام داد که قبلاً مورد بحث قرار داده شد. اگر سازنده با کلمه کلیدی New
فراخوانی شود، this
در داخل بدنه سازنده نمونهای از خود تابع سازنده خواهد بود. در اینجا، نحوه کدگذاری آن آمده است.
function Animal(name, energy) {
if (!(this instanceof Animal)) {
console.warn('Forgot to call Animal with the new keyword')
}
this.name = name
this.energy = energy
}
با این حال، به جای اینکه صرفاً هشداری صادر شود، اگر بتوان تابع را با کلمه کلیدی New
دوباره به صورت زیر فراخوانی کرد، چه اتفاقی میافتد؟
function Animal(name, energy) {
if (!(this instanceof Animal)) {
return new Animal(name, energy)
}
this.name = name
this.energy = energy
}
با این پیادهسازی، Animal
چه با کلمه کلیدی New
فراخوانی شود یا نه، به درستی عمل میکند.
ایجاد دوباره Object.create
در طول این بحث، به طور گسترده از Object.create
برای ایجاد اشیایی استفاده شد که در صورت شکست جستجو، به نمونه اولیه تابع سازنده واگذار میشوند. در حال حاضر، میدانیم که چگونه از Object.create
در کدهای خود استفاده کنیم. با این حال، ممکن است در مورد عملکرد داخلی Object.create
سوالاتی مطرح شود.
برای درک واقعی عملکرد آن، بازسازی Object.create
از ابتدا لازم است. تجزیه و تحلیل آنچه قبلاً در مورد نحوه عملکرد Object.create
بیان شد به صورت زیر بود:
- Object.create
شیئی را به عنوان استدلال میپذیرد.
- Object.create
شیئی ایجاد میکند که در جستجوهای ناموفق به شیء آرگومان تفویض میشود.
- Object.create
شی جدید ایجاد شده را برمیگرداند.
مورد اول به صورت زیر است:
Object.create = function (objectToDelegateTo) {
}
این مورد مشخص شد، اما در مورد نکته دوم باید شیئی به وجود آید که در جستجوهای ناموفق به شیء آرگومان واگذار شود. این کمی پیچیدهتر است. برای انجام این کار باید درک خود را از نحوه عملکرد کلمه کلیدی New
و Prototype در جاوا اسکریپت به کار بگیریم. ابتدا باید تابعی خالی در اجرای Object.create
ایجاد شود. سپس باید شی آرگومان را به نمونه اولیه این تابع اختصاص داد. پس از آن، با فراخوانی تابع خالی با کلمه کلیدی New
شی جدید ایجاد میشود. اگر این شی جدید بازگرداند شود، نکته سوم نیز پوشش داده خواهد شد.
Object.create = function (objectToDelegateTo) {
function F() {}
F.prototype = objectToDelegateTo;
return new F();
}
در کدهای فوق، هنگامی که تابع جدید، F در کدها ایجاد میشود، دارای ویژگی Prototype خود است. اگر این تابع با کلمه کلیدی New
فراخوانی شود، شیئی دریافت خواهد شد که در صورت شکست جستجو، به نمونه اولیه تابع منتقل میشود. با نادیده گرفتن نمونه اولیه تابع، میتوان تصمیم گرفت که کدام شی باید برای تفویض اختیار در طول جستجوهای ناموفق استفاده شود. از این رو، در کدهای فوق، نمونه اولیه F
با شیء ارسال شده در هنگام فراخوانی Object.create
، به نام objectToDelegateTo
لغو میشود.
باید به این نکته توجه کرد که این نسخه از Object.create
تنها از یک آرگومان پشتیبانی میکند. پیادهسازی اصلی از آرگومان اختیاری دوم پشتیبانی کرده است که به کاربر امکان میدهد ویژگی های اضافی را به شی ایجاد شده اضافه کند.
توابع پیکان
توابع پیکان فاقد کلمه کلیدی this
خود هستند. در نتیجه، آنها نمیتوانند به عنوان توابع سازنده عمل کنند. تلاش برای فراخوانی تابع پیکان با کلمه کلیدی New
منجر به خطا میشود.
const Animal = () => {}
const leo = new Animal() // Error: Animal is not a constructor
علاوه بر این، از آنجا که ثابت شد الگوی شبه کلاسیک با «توابع پیکان» (Arrow Function) ناسازگار است، این توابع همچنین دارای ویژگی prototype در جاوا اسکریپت نیستند.
const Animal = () => {}
console.log(Animal.prototype) // undefined
سخن پایانی
همانطور که بیان شد، Prototype در جاوا اسکریپت مکانیزم و ساز و کاری محسوب میشود که به وسیله آن اشیا ویژگیهایی را از یکدیگر به ارث میبرند که به این وراثت، نمونه اولیه میگویند. در مطلب فوق از مجله تم آف بحث Prototype در جاوا اسکریپ با استفاده از زبانی ساده و مثالهای متعددی مورد بحث قرار گرفت. یادگیری این مفهوم به کاربران کمک میکند کدهای خواناتر و واضحتری بنویسند و این شیوه، بهرهوری کلی را در برنامه نویسی جاوا اسکریپت افزایش میدهد.
در مطلب فوق سناریوهای مختلفی برای Prototype در جاوا اسکریپت و نحوه تعامل با آن ارائه شد که عمیق شدن در آنها به تسلط کاربران در این مفهوم در برنامه نویسی جاوا اسکریپت کمک زیادی میکند. به امید اینکه مطلب فوق برای کاربران عزیز مفید واقع شده باشد.