جاوا اسکریپت یک زبان برپایه prototype است. به این معنی که ویژگیها و متدهای آبجکت را میتوان از طریق آبجکت تعمیمیافته (generalized objects) که قابلیت کلونسازی و گسترش دارند به اشتراک گذاشت. این به عنوان وراثت prototype شناخته می شود و با وراثت کلاس متفاوت است. در میان زبان های برنامه نویسی شی گرا محبوب، جاوا اسکریپت منحصر به فرد است، زیرا زبان های دیگری مانند PHP، پایتون و جاوا زبان های مبتنی بر کلاس هستند.
Prototype های جاوا اسکریپت
هر آبجکت جاوا اسکریپت دارای یک ویژگی داخلی به نام [[Prototype]]
است. این رو میشه با یک تعریف یک آبجکت خالی نشان داد.
let x = {}
روش دیگر برای تعریف آبجک استفاده از سازنده آبجک است: let x = new Object()
برای پیدا کردن [[Prototype]]
ما از getPrototypeOf()
استفاده می کنیم.
Object.getPrototypeOf(x)
خروجی شامل متد ها و ویژگی های متفاوت داخلی است.
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
روش دیگر برای پیدا کردن [[Prototype]]
استفاده از x.__proto__;
است.
همه آبجکت هایی که ایجاد می کنیم دارای [[Prototype]]
هستند، مانند آبجکت های داخلی، مانند Date
و Array
.
وراثت Prototype
هنگامی که سعی می کنیم به یک ویژگی یا متد یک آبجکت دسترسی پیدا کنیم، جاوا اسکریپت ابتدا روی خود آبجکت جستجو می کند و اگر پیدا نشد، [[Prototype]]
آبجکت را جستجو می کند. اگر پس از جستوجو در خود آبجکت و [[Prototype]]
آن، باز هم مطابقتی یافت نشد، جاوا اسکریپت نمونه اولیه آبجکت پیوند داده شده را بررسی می کند و تا پایان زنجیره prototype به جستجو ادامه می دهد.
در انتهای زنجیره نمونه اولیه Object.prototype قرار دارد. همه اشیا خواص و متدهای Object را به ارث می برند.
در مثال ما، x
یک شی خالی است که از Object
به ارث می برد. x
می تواند از هر ویژگی یا متدی که Object
دارد، مانند toString()
استفاده کند.
x.toString() // [object Object]
زنجیره این prototype فقط یک طول دارد. x
-> Object
. اگر ما دو ویژگی [[Prototype]]
را باهم زنجیر کنیم در حقیقت به null
میرسیم.
x.__proto__.__proto__ // null
بیایید به نوع دیگری از آبجکت نگاه کنیم. اگر تجربه کار با آرایهها در جاوا اسکریپت را دارید، میدانید که آنها متدهای داخلی زیادی مانند pop()
و push()
دارند. دلیل دسترسی شما به این متدها هنگام ایجاد یک آرایه جدید این است که هر آرایه ای که ایجاد می کنید به ویژگی ها و متدهای موجود در Array.prototype
دسترسی دارد.
let y = [] // another way: let y = new Array()
y.__proto__ // [constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]
اگر به وجود constructor
در ویژگی های prototype این Array()
دقت کنیم تابع سازنده (constructor function) آبجکت را برمی گرداند. این مکانیزمی برای ساخت آبجکت ها از تابع است.
الان میتوانیم دو prototype را زنجیر کنیم بخاطر اینکه دراین مورد زنجیره من طولانی تر است. مثل y
-> Array
-> Object
y.__proto__.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
این زنجیره اکنون به Object.prototype
اشاره می کند. میتوانیم [[Prototype]]
داخلی را در برابر ویژگی prototype تابع سازنده آزمایش کنیم تا ببینیم که آنها به یک چیز اشاره میکنند یا نه.
y.__proto__ === Array.prototype // true
y.__proto__.__proto__ === Object.prototype // true
// another way
Array.prototype.isPrototypeOf(y) // true
Object.prototype.isPrototypeOf(Array) // true
// another way
y instanceof Array // true
توابع سازنده
توابع سازنده توابعی هستند که برای ساخت آبجکت های جدید مورد استفاده قرار میگیرند.
از عملگر new
برای برای ساخت نمونه جدید بر پایه تابع سازنده استفاده میشود. شاید بعضی از آبجک های داخلی خود جاوا اسکریپت را دیده باشید مثل new Date()
و new Array()
ولی ما میتواینم آبجک های شخصی خودمان را نیز بسازیم.
به عنوان مثال، فرض کنید در حال ایجاد یک بازی بسیار ساده و مبتنی بر متن هستیم. کاربر می تواند یک شخصیت را انتخاب کند و سپس انتخاب کند که چه کلاس شخصیتی مانند جنگجو، شفا دهنده، دزد و... داشته باشد.
از آنجایی که هر کاراکتر دارای ویژگی های بسیاری است، مانند داشتن یک نام، یک سطح و امتیاز، ایجاد یک سازنده به عنوان یک الگو منطقی است. با این حال، از آنجایی که هر کلاس شخصیت ممکن است توانایی های بسیار متفاوتی داشته باشد، ما می خواهیم مطمئن شویم که هر شخصیت فقط به توانایی های خود دسترسی دارد. بیایید نگاهی بیندازیم که چگونه می توانیم این کار را با وراثت prototype و constructor ها انجام دهیم.
یک تابع سازنده همان تابع معمولی در جاوا اسکریپت است. زمانی که آنرا با عملگر new
نمونه سازی میکنیم. طبق یک قرارداد اولین حرف نام تابع سازنده را با حروف بزرگ مینویسیم.
// Initialize a constructor function for a new Hero
function Hero(name, level) {
this.name = name
this.level = level
}
ما تابع سازنده Hero
را با دو مولفه name
و level
ساختیم. و تمام کاراکترهای ما قرار است این دو خواصیت را داشته باشند. کلمه کلیدی this
به نمونه جدیدی که ساخته شده است اشاره می کند. بنابراین this.name
این تضمین را میدهد که آبجکت جدید دارای پارامتر name
خواهد بود.
خب یک نمونه جدید میسازیم:
let hero1 = new Hero('Bjorn', 1) // Output: Hero {name: "Bjorn", level: 1}
حال اگر [[Prototype]]
آبجکت hero1
را نگاه کنیم constructor با نام Hero()
خواهید دید.
Object.getPrototypeOf(hero1) // Output constructor: ƒ Hero(name, level)
// another way
ما یک متد جدید با نام greet()
در prototype
آبجکت Hero
میسازیم.
// Add greet method to the Hero prototype
Hero.prototype.greet = function () {
return `${this.name} says hello.`
}
hero1.greet() // Output: "Bjorn says hello."
ما میتوانیم با استفاده از متد call()
ویژگی ها را از یک سازنده به سازنده دیگر کپی کنیم.
// Initialize Warrior constructor
function Warrior(name, level, weapon) {
// Chain constructor with call
Hero.call(this, name, level)
// Add a new property
this.weapon = weapon
}
// Initialize Healer constructor
function Healer(name, level, spell) {
Hero.call(this, name, level)
this.spell = spell
}
الان هردو سازنده ویژگی های Hero
را دارا هستند. و متد های خاص تر attack()
را به Warrior
و متد heal()
را به Healer
اضافه میکنیم.
Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`
}
Healer.prototype.heal = function () {
return `${this.name} casts ${this.spell}.`
}
الان با استفاده از دو کلاس کاراکتر جدید کاراکترهای خود را تعریف میکنیم.
const hero1 = new Warrior('Bjorn', 1, 'axe') // Output: Warrior {name: "Bjorn", level: 1, weapon: "axe"}
const hero2 = new Healer('Kanin', 1, 'cure') // Output: Healer {name: "Kanin", level: 1, weapon: "cure"}
hero1
اکنون با ویژگی های جدید به عنوان Warrior
شناخته می شود.
حال ما میتوانیم از پروتوتایپ Warrior
استفاده کنیم.
hero1.attack() // Console: "Bjorn attacks with the axe."
اما چه اتفاقی میافتد اگر بخواهیم از متدهای prototype بالاتر استفاده کنیم؟
hero1.greet() // Output: Uncaught TypeError: hero1.greet is not a function
ویژگی ها و متدهای prototype زمانی که از call()
استفاده می کنیم یصورت اتوماتیک لینک نمی شوند.
برای لینک کردم ویژگی های سازنده Hero
به سازنده های Warrior
و Healer
باید از Object.setPropertyOf()
استفاده کنیم.
Object.setPrototypeOf(Warrior.prototype, Hero.prototype)
Object.setPrototypeOf(Healer.prototype, Hero.prototype)
// All other prototype methods added below
اکنون میتوانیم با موفقیت از متدهای prototype آبجکت Hero
در نمونهای از Warrior
یا Healer
استفاده کنیم.
hero1.greet() // Output: "Bjorn says hello."
کد کامل پروژه کاراکتر سازی ما:
// Initialize constructor functions
function Hero(name, level) {
this.name = name
this.level = level
}
function Warrior(name, level, weapon) {
Hero.call(this, name, level)
this.weapon = weapon
}
function Healer(name, level, spell) {
Hero.call(this, name, level)
this.spell = spell
}
// Link prototypes and add prototype methods
Object.setPrototypeOf(Warrior.prototype, Hero.prototype)
Object.setPrototypeOf(Healer.prototype, Hero.prototype)
Hero.prototype.greet = function () {
return `${this.name} says hello.`
}
Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`
}
Healer.prototype.heal = function () {
return `${this.name} casts ${this.spell}.`
}
// Initialize individual character instances
const hero1 = new Warrior('Bjorn', 1, 'axe')
const hero2 = new Healer('Kanin', 1, 'cure')
با این کد ما با سازنده Hero
با پایه ترین ویژگی ها دو کاراکتر سازنده به نام های Warrior
و Healer
ساختیم و به prototype های آنها متدهایی را اضافه کردیم.
نتیجه
جاوا اسکریپت یک زبان مبتنی بر prototype است و عملکرد متفاوتی با پارادایم مبتنی بر کلاس دارد که بسیاری از زبان های شی گرا دیگر از آن استفاده می کنند.
در این آموزش، یاد گرفتیم که چگونه prototype ها در جاوا اسکریپت کار میکنند، و چگونه میتوان ویژگیها و متدهای آبجکت را از طریق ویژگی پنهان [[Prototype]]
که همه آبجکت به اشتراک میگذارند، پیوند دهیم. ما همچنین یاد گرفتیم که چگونه توابع سازنده سفارشی ایجاد کنیم و چگونه وراثت prototype برای انتقال ویژگی ها و متد ها کار می کند.