آشنایی با Prototype ها و وراثت در جاوا اسکریپت<!-- --> - Husen's Blog

آشنایی با Prototype ها و وراثت در جاوا اسکریپت

جاوا اسکریپت یک زبان برپایه 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 برای انتقال ویژگی ها و متد ها کار می کند.