۲۱ ژوئیه ۲۰۲۲

پروتوتایپ‌های نیتیو (Native prototypes)

ویژگی "prototype" به طور گسترده توسط هسته خود جاوااسکریپت استفاده می‌شود. تمام تابع‌های سازنده درون‌ساخت از آن استفاده می‌کنند.

در ابتدا به جزئیات می‌پردازیم و سپس چگونگی استفاده کردن از آن برای اضافه کردن قابلیت‌های جدید به شیءهای درون‌ساخت را بررسی می‌کنیم.

ویژگی Object.prototype

فرض کنیم ما یک شیء خالی را خروجی می‌گیریم:

let obj = {};
alert( obj ); // "[object Object]" ?

کدی که رشته "[object Object]" را ایجاد می‌کند کجاست؟ این کد یک متد toString درون‌ساخت است اما کجاست؟ obj خالی است!

…اما نماد کوتاه obj = {} با obj = new Object() یکسان است، که Object یک تابع سازنده درون‌ساخت شیء است، که دارای prototype است که به یک شیء بسیار بزرگ حاوی toString و متدهای دیگر رجوع می‌کند.

اینجا می‌بینیم که چه اتفاقی در حال رخ دادن است:

زمانی که new Object() فراخوانی شود (یا یک شیء لیترال {...} ساخته می‌شود)، با توجه به قانونی که ما در فصل قبلی درباره آن صحبت کردیم، [[Prototype]] آن در Object.prototype قرار داده می‌شود:

سپس زمانی که obj.toString() فراخوانی می‌شود، این متد از Object.prototype گرفته می‌شود.

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

let obj = {};

alert(obj.__proto__ === Object.prototype); // true

alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true

لطفا در نظر داشته باشید که [[Prototype]] دیگری در زنجیره بالای Object.prototype وجود ندارد:

alert(Object.prototype.__proto__); // null

دیگر پروتوتایپ‌های درون‌ساخت

شیءهای دیگر درون‌ساخت مانند Array، Date، Function و بقیه هم متدهایی درون پروتوتایپ‌ها ذخیره می‌کنند.

برای مثال، زمانی که ما آرایه [1, 2, 3] را می‌سازیم، سازنده new Array() به صورت درونی استفاده می‌شود. پس Array.prototype پروتوتایپ آن می‌شود و متدها را فراهم می‌کند. این کار برای حافظه خیلی کارآمد است.

با توجه به خصوصیات زبان، تمام پروتوتایپ‌ها، Object.prototype را بالای خود دارند. به همین دلیل است که بعضی افراد می‌گویند «همه چیز از شیءها ارث‌بری می‌کنند».

اینجا یک تصویر کلی داریم (برای 3 سازنده درون‌ساخت تا جا شود):

بیایید به صورت دستی پروتوتایپ‌ها را بررسی کنیم:

let arr = [1, 2, 3];

// ارث‌بری می‌کند؟ Array.prototype آیا از
alert( arr.__proto__ === Array.prototype ); // true

// چطور؟ Object.prototype سپس از
alert( arr.__proto__.__proto__ === Object.prototype ); // true

// قرار دارد null و در بالا
alert( arr.__proto__.__proto__.__proto__ ); // null

بعضی از متدها در پروتوتایپ‌ها ممکن است با هم تطابق داشته باشند، برای مثال Array.prototype متد toString خودش را دارد که المان‌ها را به صورت جداشده توسط کاما لیست می‌کند:

let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- Array.prototype.toString نتیجه‌ی

همانطور که قبلا دیدیم، Object.prototype هم متد toString را دارد اما Array.prototype در زنجیره نزدیک‌تر است پس نوع آرایه آن استفاده می‌شود.

ابزارهای درون مرورگر مانند کنسول توسعه‌دهنده کروم هم ارث‌بری را نشان می‌دهند (ممکن است برای شیءهای درون‌ساخت نیاز باشد که console.dir استفاده شود):

بقیه شیءهای درون‌ساخت هم این چنین کار می‌کنند. حتی تابع‌ها – آن‌ها شیءهایی از سازنده Function هستند و متدهای آن‌ها (call/apply و بقیه) از Function.prototype گرفته می‌شوند. تابع‌ها toString خودشان را هم دارند.

function f() {}

alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true ،ارث‌بری از شیءها

مقدارهای اصلی

پیچیده‌ترین چیزی که با رشته‌ها، عددها و بولین‌ها اتفاق می‌افتد.

همانطور که به یاد داریم، آن‌ها شیء نیستند. اما اگر سعی کنیم که به ویژگی‌های آن‌ها دسترسی پیدا کنیم، شیءهای دربرگیرنده موقتی با استفاده از سازنده‌های درون‌ساخت String، Number و Boolean ساخته می‌شوند. آن‌ها متدها را فراهم می‌کنند و سپس ناپدید می‌شوند.

این شیءها به صورت پنهانی ایجاد می‌شوند و بیشتر موتورها آن‌ها را بهینه می‌کنند اما خصوصیات زبان دقیقا به همین صورت آن‌ها را توصیف می‌کند. متدهای این شیءها هم درون پروتوتایپ‌ها قرار دارند و به صورت String.prototype، Number.prototype و Boolean.prototype در دسترس هستند.

مقدارهای null و undefined دارای دربرگیرنده شیء نیستند

مقدارهای خاص null و undefined استثنا هستند. آن‌ها دربرگیرنده شیء ندارند پس متدها و ویژگی‌هایی هم برای آن‌ها موجود نیست. و پروتوتایپ‌های متناظر هم وجود ندارد.

تغییر پروتوتایپ‌های نیتیو

پروتوتایپ‌های نیتیو (Native prototypes) می‌توانند تغییر کنند. برای مثال، اگر ما متدی را به String.prototype اضافه کنیم، این متد برای تمام رشته‌ها در دسترس خواهد بود:

String.prototype.show = function() {
  alert(this);
};

"BOOM!".show(); // BOOM!

در حین فرایند توسعه، ممکن است ایده‌هایی برای متدهای درون‌ساخت جدیدی به ذهن‌مان برسد که بخواهیم آن‌ها را داشته باشیم و ممکن است مشتاق باشیم که آن‌ها را به پروتوتایپ‌های نیتیو اضافه کنیم. اما به طور کلی این کار بدی است.

مهم:

پروتوتایپ‌ها گلوبال هستند، پس دریافت تناقض آسان است. اگر دو کتابخانه متد String.prototype.show را اضافه کنند، یکی از آن‌ها ممکن است متد دیگری را بازنویسی کند.

پس به طور کلی، تغییر یک پروتوتایپ نیتیو کار بدی محسوب می‌شود.

در برنامه‌نویسی مدرن، فقط یک مورد است که تغییر دادن پروتوتایپ‌های نیتیو قابل قبول است. آن هم پلیفیل‌سازی است.

پلیفیل‌سازی (polyfilling) عبارتی برای ایجاد یک جایگزین برای متدی است که در خصوصیات جاوااسکریپت وجود دارد اما هنوز توسط موتور جاوااسکریپت خاصی پشتیبانی نمی‌شود.

سپس می‌توانیم آن را به صورت دستی پیاده‌سازی و به پروتوتایپ درون‌ساخت اضافه کنیم.

برای مثال:

if (!String.prototype.repeat) { // اگر چنین متدی وجود نداشته باشد
  // آن را به پروتوتایپ اضافه کن

  String.prototype.repeat = function(n) {
    // بار تکرار کن n رشته را

    // در واقع کد باید نسبت به این کمی بیشتر پیچیده باشد
    // (الگوریتم کامل درون خصوصیات زبان موجود است)
    // اما حتی یک پلیفیل ناکامل هم اغلب اوقات کافی است
    return new Array(n + 1).join(this);
  };
}

alert( "La".repeat(3) ); // LaLaLa

قرض گرفتن از پروتوتایپ‌ها

در فصل دکوراتورها و ارسال کردن، متدهای call/apply ما درباره قرض گرفتن متد صحبت کردیم.

این زمانی است که ما متدی را از یک شیء دریافت می‌کنیم و آن را درون شیء دیگری کپی می‌کنیم.

بعضی از متدهای پروتوتایپ‌های نیتیو اغلب اوقات قرض گرفته می‌شوند.

برای مثال، اگر ما در حال ساخت یک شیء آرایه مانند باشیم، ممکن است بخواهیم بعضی از متدهای Array را درون آن کپی کنیم.

برای مثال:

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join;

alert( obj.join(',') ); // Hello,world!

این کد کار می‌کند چون الگوریتم داخلی متد درون‌ساخت join فقط به ایندکس‌های درست و ویژگی length اهمیت می‌دهد. این متد بررسی نمی‌کند که شیء واقعا یک آرایه باشد. بسیاری از متدهای درون‌ساخت همینگونه هستند.

احتمال دیگر هم ارث‌بری است که از طریق برابر قرار دادن obj.__proto__ با Array.prototype انجام می‌شود، پس تمام متدهای Array به صورت خودکار درون obj قابل دسترسی خواهند بود.

اما اگر obj از قبل از شیء دیگری ارث‌بری کند این کار ناممکن می‌شود. به یاد داشته باشید، ما به طور همزمان فقط می‌توانیم از یک شیء ارث‌بری کنیم.

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

خلاصه

  • تمام شیءهای درون‌ساخت الگویی یکسان را دنبال می‌کنند:
    • متدها درون پروتوتایپ ذخیره شده‌اند (Array.prototype، Object.prototype، Date.prototype و غیره.)
    • The object itself stores only the data (array items, object properties, the date)
    • خود شیء فقط داده را ذخیره می‌کند (المان‌های آرایه، ویژگی‌های شیء، تاریخ)
  • مقدارهای اصلی هم متدها را درون پروتوتایپ‌های شیءهای دربرگیرنده ذخیره می‌کنند : Number.prototype، String.prototype و Boolean.prototype. فقط undefined و null شیءهای دربرگیرنده ندارند.
  • پروتوتایپ‌های درون‌ساخت می‌توانند تغییر کنند یا با متدهای جدید پر شوند. اما پیشنهاد نمی‌شود که آن‌ها را تغییر دهید. تنها موردی که مجاز است احتمالا زمانی است که ما می‌خواهیم یک استاندارد جدید را اضافه کنیم اما هنوز توسط موتور جاوااسکریپت پشتیبانی نشده است.

تمارین

اهمیت: 5

متد defer(ms) را به پروتوتایپ تمام تابع‌ها اضافه کنید که تابع را بعد از ms میلی‌ثانیه اجرا می‌کند.

بعد از اینکه آن را انجام دادید، چنین کدی باید کار کند:

function f() {
  alert("Hello!");
}

f.defer(1000); // را نشان می‌دهد "Hello!" بعد از 1 ثانیه
Function.prototype.defer = function(ms) {
  setTimeout(this, ms);
};

function f() {
  alert("Hello!");
}

f.defer(1000); // را نشان می‌دهد "Hello!" بعد از 1 ثانیه
اهمیت: 4

متد defer(ms) را به پروتوتایپ تمام تابع‌ها اضافه کنید که یک دربرگیرنده را برمی‌گرداند و فراخوانی را به اندازه ms میلی‌ثانیه به تاخیر می‌اندازد.

این کد نمونه‌ای چگونگی کار کردن آن است:

function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // بعد از 1 ثانیه 3 را نمایش می‌دهد

لطفا در نظر داشته باشید که آرگومان‌ها باید به تابع اصلی پاس داده شوند.

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

// آن را بررسی کنید
function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // بعد از 1 ثانیه 3 را نمایش می‌دهد

لطفا توجه کنید: ما در f.apply از this استفاده کردیم تا کاری کنیم که دکور کردن برای متدهای شیء هم کار کند.

پس اگر تابع دربرگیرنده به عنوان متد شیء فراخوانی شود، سپس this به متد اصلی f پاس داده می‌شود.

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

let user = {
  name: "John",
  sayHi() {
    alert(this.name);
  }
}

user.sayHi = user.sayHi.defer(1000);

user.sayHi();
نقشه آموزش