زنجیره‌ی وراثت پنهان در جاوااسکریپت: Prototype و Prototype Chain

مقدمه

تا اینجا با شیءها به‌عنوان مجموعه‌ای مستقل از Property ها و متدها کار کرده‌ایم. اما یک سؤال مهم مطرح است: وقتی روی یک آرایه متد push را صدا می‌زنیم، این متد از کجا می‌آید؟ ما هرگز آن را روی آرایه تعریف نکرده‌ایم. پاسخ این سؤال، مکانیزمی است به نام Prototype که پایه‌ی اصلی وراثت (Inheritance) در جاوااسکریپت است.

مشکل: تکرار کد بین شیءهای مشابه

فرض کنید چند شیء داریم که همگی باید یک متد مشترک داشته باشند:

let user1 = {
  name: "علی",
  greet: function () {
    console.log("سلام، من " + this.name + " هستم");
  }
};

let user2 = {
  name: "سارا",
  greet: function () {
    console.log("سلام، من " + this.name + " هستم");
  }
};

در این کد، تابع greet عملاً دوبار نوشته شده؛ برای هر شیء یک نسخه‌ی کاملاً مستقل. اگر صدها شیء مشابه داشته باشیم، این تکرار هم حافظه‌ی زیادی مصرف می‌کند و هم نگه‌داری کد را دشوار می‌کند. Prototype دقیقاً راه‌حل جاوااسکریپت برای این مشکل است.

هر شیء یک Prototype دارد

در جاوااسکریپت، تقریباً هر شیء یک ارجاع پنهان به شیء دیگری دارد که به آن Prototype آن شیء می‌گویند. وقتی به دنبال یک Property یا متد روی یک شیء می‌گردیم و آن را مستقیماً روی خودِ شیء پیدا نمی‌کنیم، جاوااسکریپت به‌طور خودکار به سراغ Prototype آن شیء می‌رود و جست‌وجو را آن‌جا ادامه می‌دهد.

دسترسی به متدی که مستقیماً روی شیء تعریف نشده
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

شیء user هرگز متدهای toString یا hasOwnProperty را مستقیماً تعریف نکرده است. اما این متدها همچنان قابل فراخوانی هستند؛ چون وقتی جاوااسکریپت آن‌ها را مستقیماً روی user پیدا نمی‌کند، به Prototype آن مراجعه می‌کند. در این مورد، Prototype پیش‌فرض یک Object Literal، همان Object.prototype است که این متدها روی آن تعریف شده‌اند.

Prototype Chain چیست؟

نکته‌ی مهم این است که خودِ Prototype هم می‌تواند Prototype مخصوص به خودش را داشته باشد؛ و این روند تا جایی ادامه پیدا می‌کند که به یک شیء برسیم که Prototype آن، null باشد. به این مسیر زنجیره‌ای از شیءها، Prototype Chain می‌گویند:

بررسی زنجیره‌ی Prototype با Object.getPrototypeOf
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل رفتار جاوااسکریپت

Object.getPrototypeOf(user) همان Object.prototype را برمی‌گرداند؛ همان جایی که متدهایی مثل toString و hasOwnProperty تعریف شده‌اند. اگر یک قدم دیگر جلو برویم و Prototype خودِ Object.prototype را بررسی کنیم، به null می‌رسیم؛ یعنی انتهای زنجیره. این زنجیره دقیقاً همان مسیری است که جاوااسکریپت هنگام جست‌وجوی یک Property، پله‌پله طی می‌کند: ابتدا خودِ شیء، سپس Prototype آن، سپس Prototype آن Prototype، و به همین ترتیب تا رسیدن به null.

این مکانیزم شباهت مفهومی زیادی به Scope Chain دارد که در فصل Execution Model دیدیم؛ در آن‌جا جاوااسکریپت برای پیدا کردن یک متغیر، از Scope داخلی به سمت Scope های بیرونی حرکت می‌کرد؛ اینجا برای پیدا کردن یک Property، از شیء به سمت Prototype های آن حرکت می‌کند. هر دو، نمونه‌ای از یک الگوی مشترک در طراحی جاوااسکریپت هستند: جست‌وجوی زنجیره‌ای در صورت نیافتن مقدار در سطح فعلی.

ساخت Prototype سفارشی با Object.create

می‌توانیم به‌طور دستی، یک شیء را با Prototype دلخواه خودمان بسازیم؛ این کار مستقیم‌ترین راه برای درک عملی Prototype است:

ساخت شیء با Prototype سفارشی
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

Object.create(animalMethods) یک شیء کاملاً جدید می‌سازد که Prototype آن، مستقیماً برابر animalMethods است. به همین دلیل، وقتی dog.eat() فراخوانی می‌شود، جاوااسکریپت ابتدا eat را مستقیماً روی dog جست‌وجو می‌کند (که پیدا نمی‌شود)، سپس به Prototype آن یعنی animalMethods می‌رود و آن‌جا پیدا می‌کند. به همین دلیل dog.hasOwnProperty("eat") مقدار false برمی‌گرداند؛ چون eat روی خودِ dog تعریف نشده، بلکه از طریق Prototype در دسترس است. اما name، چون مستقیماً روی dog تنظیم شده، hasOwnProperty برای آن true برمی‌گرداند.

تفاوت Property مستقیم و Property به‌ارث‌رسیده

متد hasOwnProperty که در مثال بالا دیدیم، دقیقاً برای همین منظور طراحی شده: تشخیص اینکه آیا یک Property مستقیماً روی خودِ شیء تعریف شده، یا از طریق Prototype Chain به ارث رسیده است. این تفاوت اهمیت عملی دارد؛ برای نمونه، حلقه‌ی for...in که در فصل حلقه‌ها دیدیم، به‌طور پیش‌فرض تمام Property های قابل شمارش را پیمایش می‌کند؛ از جمله آن‌هایی که از Prototype Chain به ارث رسیده‌اند، نه فقط Property های مستقیم شیء.

this در متدهای به‌ارث‌رسیده از Prototype

نکته‌ی جالب توجه در مثال dog.eat() این است که با اینکه متد eat روی animalMethods تعریف شده، اما وقتی از طریق dog.eat() فراخوانی می‌شود، this داخل آن برابر dog است، نه animalMethods. این دقیقاً همان قانون Implicit Binding است که در فصل this و Binding دیدیم: مقدار this بر اساس شیئی تعیین می‌شود که بلافاصله قبل از نقطه در فراخوانی قرار دارد، نه بر اساس اینکه متد اصلاً کجا تعریف شده است. به همین دلیل بود که this.name در متد eat، به‌درستی "سگ" را چاپ کرد.

جدول جمع‌بندی

مفهومتوضیح
Prototypeشیء دیگری که یک شیء، در صورت نیافتن Property، به آن مراجعه می‌کند
Prototype Chainزنجیره‌ای از Prototype ها که جست‌وجو تا رسیدن به null در آن ادامه می‌یابد
Object.create(proto)ساخت یک شیء جدید با Prototype دلخواه
hasOwnPropertyبررسی اینکه آیا یک Property مستقیماً روی شیء است یا از Prototype به ارث رسیده

جمع‌بندی

Prototype مکانیزمی است که به شیءهای جاوااسکریپت اجازه می‌دهد Property ها و متدها را بدون کپی کردن مستقیم، از شیء دیگری به ارث ببرند. وقتی یک Property مستقیماً روی شیء پیدا نشود، جاوااسکریپت آن را در Prototype Chain -یعنی زنجیره‌ای از شیءهای مرتبط به یکدیگر- جست‌وجو می‌کند تا به null برسد. Object.create امکان ساخت دستی یک شیء با Prototype مشخص را فراهم می‌کند، و hasOwnProperty برای تشخیص Property های مستقیم از Property های به‌ارث‌رسیده استفاده می‌شود.