۱۵ دسامبر ۲۰۲۱

F.prototype

به خاطر داشته باشید، شیء‌های جدید را می‌توان با یک تابع سازنده ایجاد کرد، مثل new F().

اگر F.prototype یک شیء باشد، عملگر new از آن برای تنظیم [[Prototype]] برای شیء جدید استفاده می‌کند.

لطفاً توجه کنید:

جاوا‌اسکریپت از ابتدا دارای وراثت پروتوتایپ بود. این یکی از ویژگی‌های اصلی زبان بود.

اما در قدیم، دسترسی مستقیم به آن وجود نداشت. تنها چیزی که به طور قابل اعتماد کار می‌کرد ویژگی "prototype" تابع سازنده بود که در این فصل توضیح داده شد. بنابراین اسکریپت‌های زیادی وجود دارند که هنوز از آن استفاده می‌کنند.

لطفاً توجه داشته باشید که F.prototype در اینجا به معنای یک ویژگی معمولی به نام "prototype" در F است. چیزی شبیه به اصطلاح “prototype” به نظر می‌رسد، اما در اینجا واقعاً به معنای یک ویژگی معمولی با این نام است.

در اینجا یک مثال وجود دارد:

let animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("خرگوش سفید"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true

تنظیم Rabbit.prototype = animal به معنای واقعی کلمه این موارد را بیان می‌کند: «وقتی یک new Rabbit ایجاد شد، [[Prototype]] آن را به animal اختصاص دهید«.

این تصویر نتیجه است:

در تصویر، "prototype" یک فلش افقی است، به معنای یک ویژگی معمولی، و [[Prototype]] عمودی است، به معنای ارث بردن rabbit از animal.

F.prototype فقط در زمان new F استقاده می‌شود

ویژگی F.prototype تنها زمانی استفاده می‌شود که new F فراخوانی شود، [[Prototype]] را به شیء جدید اختصاص می‌دهد.

اگر پس از ایجاد، ویژگی F.prototype تغییر کند (F.prototype = <یک شیء دیگر>)، آنگاه اشیاء جدید ایجاد شده توسط new F شیء دیگری به عنوان [[Prototype]] خواهند داشت، اما اشیاء موجود، شیء قدیمی را حفظ می‌کنند.

F.prototype پیش‌فرض، ویژگی سازنده

هر تابع دارای ویژگی "prototype" است، حتی اگر آن را تنظیم نکنیم.

"prototype" پیش‌فرض یک شیء با تنها ویژگی constructor است که به خود تابع اشاره می‌کند.

مثل این:

function Rabbit() {}

/* پیش‌فرض prototype
Rabbit.prototype = { constructor: Rabbit };
*/

ما می‌توانیم آن را بررسی کنیم:

function Rabbit() {}
// به صورت پیش‌فرض:
// Rabbit.prototype = { constructor: Rabbit }

alert( Rabbit.prototype.constructor == Rabbit ); // true

طبیعتاً، اگر کاری انجام ندهیم، ویژگی constructor از طریق [[Prototype]] برای همه خرگوش‌ها در دسترس است:

function Rabbit() {}
// به صورت پیش‌فرض:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // {constructor: Rabbit} ارث می‌برد از

alert(rabbit.constructor == Rabbit); // true (از پروتوتایپ)

می‌توانیم از ویژگی constructor برای ایجاد یک شیء جدید با استفاده از سازنده مشابه موجود استفاده کنیم.

مثل این نمونه:

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("خرگوش سفید");

let rabbit2 = new rabbit.constructor("خرگوش سیاه");

این مفید است زمانی که ما یک شی داریم، نمی‌دانیم از کدام سازنده برای آن استفاده شده است (مثلاً از یک کتابخانه شخص ثالث می‌آید)، و باید یکی دیگر از همان نوع ایجاد کنیم.

اما احتمالاً مهمترین چیز در مورد "constructor" این است که …

*** …خود جاوااسکریپت از مقدار درست "constructor" اطمینان نمی‌دهد.**

بله، در "prototype" پیش‌فرض برای توابع وجود دارد، اما همین. آنچه بعداً با آن اتفاق می افتد – کاملاً به ما بستگی دارد.

به ویژه، اگر پروتوتایپ پیش‌فرض را به‌طور کلی جایگزین کنیم، "constructor" در آن وجود نخواهد داشت.

برای مثال:

function Rabbit() {}
Rabbit.prototype = {
  jumps: true
};

let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false

بنابراین، برای حفظ "constructor" درست، می‌توانیم به جای بازنویسی به‌عنوان یک کل، ویژگی‌ها را به "prototype" پیش‌فرض اضافه یا از آن حذف کنیم:

function Rabbit() {}

// را به طور کامل بازنویسی نکنید Rabbit.prototype
// فقط به آن اضافه کنید
Rabbit.prototype.jumps = true
// حفظ می‌شود Rabbit.prototype.constructor حالت پیش‌فرض

یا، به طور متناوب، ویژگی constructor را به صورت دستی دوباره ایجاد کنید:

Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};

// حالا سازنده نیز درست است، زیرا ما دوباره آن را اضافه کردیم

خلاصه

در این فصل به طور خلاصه نحوه تنظیم [[Prototype]] برای اشیاء ایجاد شده از طریق یک تابع سازنده را توضیح دادیم. در آینده شاهد الگوهای برنامه‌نویسی پیشرفته‌تری خواهیم بود که بر آن تکیه دارند.

همه چیز بسیار ساده است، فقط چند نکته برای روشن شدن همه چیز‌ها:

  • ویژگی F.prototype (آن را با [[Prototype]] اشتباه نگیرید)، [[Prototype]] را برای اشیاء جدید، هنگام فراخوانی new F() تنظیم می‌کند.
  • مقدار F.prototype باید یک شیء یا nullباشد: مقادیر دیگر کار نمی‌کنند.
  • ویژگی "prototype" تنها زمانی چنین جلوه خاصی دارد که روی یک تابع سازنده تنظیم شود و با new فراخوانی شود.

در اشیاء معمولی prototype چیز خاصی نیست:

let user = {
  name: "John",
  prototype: "Bla-bla" // به هیچ صورت جادویی نیست
};

به‌طور پیش‌فرض همه توابع دارای F.prototype = { constructor: F } هستند، بنابراین می‌توانیم سازنده یک شیء را با دسترسی به ویژگی "constructor" آن دریافت کنیم.

تمارین

اهمیت: 5

در کد زیر new Rabbit را ایجاد می‌کنیم و سپس سعی می‌کنیم پروتوتایپ آن را تغییر دهیم.

در شروع، ما این کد را داریم:

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

alert( rabbit.eats ); // true
  1. یک رشته دیگر اضافه کردیم (تاکید شده). اکنون alert چه چیزی را نشان می‌دهد؟

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype = {};
    
    alert( rabbit.eats ); // ?
  2. …و اگر کد به این صورت باشد (یک خط جایگزین شده است)؟

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype.eats = false;
    
    alert( rabbit.eats ); // ?
  3. و مثل این (یک خط جایگزین شده است)؟

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete rabbit.eats;
    
    alert( rabbit.eats ); // ?
  4. آخرین نوع:

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete Rabbit.prototype.eats;
    
    alert( rabbit.eats ); // ?

پاسخ‌ها:

  1. true.

    انتساب به Rabbit.prototype ،[[Prototype]] را برای اشیاء جدید تنظیم می‌کند، اما بر موارد موجود تأثیری نمی‌گذارد.

  2. false.

    اشیاء با مرجع تخصیص داده می‌شوند. شیء Rabbit.prototype تکراری نیست، همچنان یک شیء واحد است که هم توسط Rabbit.prototype و هم توسط [[Prototype]] از rabbit ارجاع داده شده است.

    بنابراین وقتی محتوای آن را از طریق یک مرجع تغییر می‌دهیم، از طریق مرجع دیگر قابل مشاهده است.

  3. true.

    تمام عملیات delete مستقیماً روی شیء اعمال می‌شود. در اینجا delete rabbit.eats سعی می‌کند ویژگی eats را از rabbit حذف کند، اما آن را ندارد. بنابراین عملیات هیچ تاثیری نخواهد داشت.

  4. undefined.

    ویژگی eats از پروتوتایپ حذف شده است، دیگر وجود ندارد.

اهمیت: 5

تصور کنید، یک شیء دلخواه obj داریم که توسط یک تابع سازنده ایجاد شده است – نمی‌دانیم کدام یک، اما می‌خواهیم با استفاده از آن یک شیء جدید ایجاد کنیم.

می‌توانیم آن را انجام دهیم؟

let obj2 = new obj.constructor();

مثالی از یک تابع سازنده برای obj بیاورید که به این کد اجازه می‌دهد درست کار کند. و مثالی که باعث می‌شود اشتباه کار کند.

اگر مطمئن باشیم که ویژگی "constructor" مقدار صحیحی دارد، می‌توانیم از چنین رویکردی استفاده کنیم.

برای مثال، اگر "prototype" پیش‌فرض را تغییر ندهیم، این کد مطمئناً کار می‌کند:

function User(name) {
  this.name = name;
}

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // Pete (کار کرد!)

کار کرد، زیرا User.prototype.constructor == User.

…اما اگر شخصی، به اصطلاح، User.prototype را بازنویسی کند و فراموش کند constructor را برای ارجاع به User بازآفرینی کند، آنگاه شکست خواهد خورد.

برای مثال:

function User(name) {
  this.name = name;
}
User.prototype = {}; // (*)

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // undefined

چرا user2.name برابر با undefined است؟

در اینجا نحوه عملکرد new user.constructor('Pete') وجود دارد:

  1. ابتدا، به دنبال constructor در user می‌گردد. هیچ‌چیز.
  2. سپس از زنجیره پروتوتایپ پیروی می‌کند. پروتوتایپ user برابر با User.prototype است، و همچنین constructor ندارد (زیرا ما «فراموش کردیم» آن را درست تنظیم کنیم!).
  3. در ادامه زنجیره، User.prototype یک شیء ساده است، پروتوتایپ آن Object.prototype داخلی است.
  4. در نهایت، برای Object.prototype داخلی، Object.prototype.constructor == Object داخلی وجود دارد. بنابراین استفاده می‌شود.

سر‌انجام، در پایان، let user2 = new Object('Pete') را داریم.

احتمالاً این چیزی نیست که ما می‌خواهیم. ما می‌خواهیم new User ایجاد کنیم، نه new Object. این نتیجه constructor گم شده است.

(فقط در صورتی که کنجکاو باشید، فراخوانی new Object(...) آرگومان خود را به یک شیء تبدیل می‌کند. این یک چیز تئوری است، در عمل هیچ‌کس new Object را با مقدار نمی‌نامد، و عموماً ما اصلاً از new Object برای ساختن اشیاء استفاده نمی‌کنیم).

نقشه آموزش