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