Chapter 6زندگی اسرارآمیز اشیاء

An abstract data type is realized by writing a special kind of program […] which defines the type in terms of the operations which can be performed on it.

Barbara Liskov, Programming with Abstract Data Types
Picture of a rabbit with its proto-rabbit

اشیاء جاوااسکریپت، در فصل 4 معرفی شدند. در فرهنگ برنامه نویسی، مفهومی وجود دارد که به آن برنامه نویسی شیء گرا گفته می شود؛ مجموعه‌ای از تکنیک ها که در آن از اشیاء (و مفاهیم مرتبط با آن) به عنوان اصل مرکزی برای سازماندهی برنامه استفاده می شود.

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

کپسوله‌سازی (Encapsulation)

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

در این روش، بعضی از اطلاعات مربوط به نحوه‌ی کارکرد یک قسمت از برنامه را می توان به صورت محلی (local) برای همان قسمت نگه داری کرد. کسی که روی قسمت‌های دیگر برنامه کار می کند نیازی نیست تا این اطلاعات را به خاطر داشته باشد یا حتی اصلا از وجودشان آگاه باشد. در صورت تغییر این جزئیات محلی، فقط لازم است کد‌هایی را به‌روز کنیم که مستقیما پیرامون آن قرار دارند.

بخش‌های مختلف این گونه برنامه‌ها، برای ارتباط با یکدیگر از رابط‌ها (interface) استفاده می کنند؛ مجموعه‌ای محدود از توابع یا متغیرها که قابلیت‌های مفیدی در سطح بالاتری از انتزاع ایجاد می کنند و جزئیات دقیق پیاده سازی‌شان را از دید استفاده‌کننده مخفی می کنند.

قسمت‌های این‌گونه برنامه‌ها را به وسیله‌ی اشیاء مدل‌سازی می کنند. رابط آن‌ها شامل مجموعه‌ای مشخص از متدها و خاصیت‌ها می است. به آن گروه از خاصیت‌ها که بخشی از رابط محسوب می شوند، عمومی (public) و آن هایی را که کدهای بیرونی نباید به آن‌ها دسترسی داشته باشند، خاصیت‌های خصوصی (private) می نامند.

بسیاری از زبان‌های برنامه نویسی راهی برای تمایز خاصیت‌های خصوصی و عمومی فراهم می سازند و از دسترسی کدهای بیرونی به خاصیت‌های خصوصی جلوگیری می کنند. جاوااسکریپت بار دیگر با انتخاب روش مینیمال، این گونه عمل نمی کند – حداقل تا الان. البته کارهایی برای افزودن این ویژگی به زبان در حال شکل گیری است.

با وجود اینکه خود زبان به صورت درونی از این تمایز پشتیبانی نمی کند، برنامه نویسان جاوااسکریپت این مفهوم را به شکل موفقیت‌آمیزی به کار می برند. معمولا برای مشخص کردن رابط‌های در دسترس از توضیحات یا مستندات برنامه استفاده می کنند. همچنین یک روش رایج برای مشخص کردن خاصیت‌های خصوصی، استفاده از کاراکتر زیرخط (_) در شروع این خاصیت‌ها است.

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

متد‌ها (Methods)

متدها خاصیت‌هایی هستند که مقدار‌های تابع را نگه‌داری می کنند؛ همین. این یک متد ساده است:

let rabbit = {};
rabbit.speak = function(line) {
  console.log(`The rabbit says '${line}'`);
};

rabbit.speak("I'm alive.");
// → The rabbit says 'I'm alive.'

معمولا متد‌ها کاری را روی شیءای که بر روی آن فراخوانی شده‌اند انجام می دهند. زمانی که یک تابع به عنوان یک متد فراخوانی می شود – به عنوان یک خاصیت جستجو می شود و بلافاصله فراخوانی می شود مانند object.method() – متغیری که this نامیده می‌شود به طور خودکار به شیءای که روی آن فراخوانی شده است اشاره می کند.

function speak(line) {
  console.log(`The ${this.type} rabbit says '${line}'`);
}
let whiteRabbit = {type: "white", speak};
let hungryRabbit = {type: "hungry", speak};

whiteRabbit.speak("Oh my ears and whiskers, " +
                  "how late it's getting!");
// → The white rabbit says 'Oh my ears and whiskers, how
//   late it's getting!'
hungryRabbit.speak("I could use a carrot right now.");
// → The hungry rabbit says 'I could use a carrot right now.'

می توانید this را یک پارامتر اضافه در نظر بگیرید که به شکلی دیگر ارسال شده است. اگر بخواهید آن آشکارا ارسال نمایید، می توانید از متد call توابع استفاده کنید. این متد مقدار this را به عنوان آرگومان اول می گیرد و دیگر آرگومان ها را به عنوان پارامترهای معمولی تفسیر می کند.

speak.call(hungryRabbit, "Burp!");
// → The hungry rabbit says 'Burp!'

به دلیل اینکه هر تابع متغیر this مربوط به خود را دارد، که مقدارش بستگی به نحوه‌ی فراخوانی آن تابع دارد، نمی توان به this محصور در قلمروی تعریف یک تابع معمولی (که با کلیدواژه‌ی function تعریف شده است) دسترسی داشت.

توابع پیکانی (arrow functions) متفاوت عمل می کنند - این توابع this را به جایی مقید نمی کنند اما می توانند متغیر this قلمروی پیرامونشان را ببینند. بنابراین، به وسیله‌ی کدی مانند مثال زیر، می توان به this از درون یک تابع محلی اشاره کرد:

function normalize() {
  console.log(this.coords.map(n => n / this.length));
}
normalize.call({coords: [0, 2, 3], length: 5});
// → [0, 0.4, 0.6]

اگر آرگومان ارسالی به تابع map را به وسیله کلیدواژه‌ی function نوشته‌ بودم، کد بالا کار نمی کرد.

Prototype ها

با دقت به کد زیر نگاه کنید.

let empty = {};
console.log(empty.toString);
// → function toString(){…}
console.log(empty.toString());
// → [object Object]

خاصیتی را از یک شیء تهی بیرون کشیده‌ام. جادوگری!

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

بنابراین نمونه‌ی اولیه‌ی (prototype) آن شیء تهی کدام است؟ بله آن prototype جد بزرگ شیء است که تقریبا پشت همه‌ی اشیاء قرار دارد: Object.prototype.

console.log(Object.getPrototypeOf({}) ==
            Object.prototype);
// → true
console.log(Object.getPrototypeOf(Object.prototype));
// → null

همانطور که حدس می زنید، object.getPrototypeOf، برای به دست آوردن prototype یک شیء استفاده می شود.

روابط بین prototype ها در اشیاء جاوااسکریپت، یک ساختار درختی را شکل می دهند که در ریشه‌ی این ساختار، Object.prototype قرار می گیرد. درون این شیء چند متد وجود دارد که در تمامی اشیاء حضور دارند، مانند toString، که عمل تبدیل یک شیء به رشته‌ را انجام می دهد.

بسیاری از اشیاء به طور مستقیم دارای Object.prototype به عنوان prototype خودشان نیستند، اما در عوض شیء دیگری دارند که مجموعه‌ی متفاوتی از خاصیت‌های پیش‌فرض را فراهم می سازد. توابع از Function.prototype و آرایه ها از Array.prototype مشتق شده اند.

console.log(Object.getPrototypeOf(Math.max) ==
            Function.prototype);
// → true
console.log(Object.getPrototypeOf([]) ==
            Array.prototype);
// → true

این گونه prototype ها خود نیز دارای یک prototype می باشند که اغلب همان Object.prototype است. پس به طور غیر مستقیم متدهایی شبیه toString را فراهم می کنند.

می توانید از متد Object.create برای ساختن یک شیء با یک prototype خاص استفاده کنید.

let protoRabbit = {
  speak(line) {
    console.log(`The ${this.type} rabbit says '${line}'`);
  }
};
let killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "killer";
killerRabbit.speak("SKREEEE!");
// → The killer rabbit says 'SKREEEE!'

خاصیتی شبیه به speak(line) در یک تعریف شیء، شکل کوتاه تعریف یک متد است. این کار خاصیتی به نام speak را تعریف کرده و یک تابع را به عنوان مقدار به آن می دهد.

“protoRabbit” به عنوان یک ظرف برای خاصیت‌های مشترک همه‌ی خرگوش‌ها استفاده می شود. یک شی مجزای rabbit (خرگوش) مثل killerRabbit، دارای خاصیت‌های اختصاصی است که فقط متعلق به خودش – و در این مثال متعلق به نوع خودش – است و نیز خاصیت‌های مشترکی دارد که آن‌ها را از نمونه‌ی اولیه‌اش می گیرد.

کلاس‌ها

سیستم prototype جاوااسکریپت را می توان به عنوان برداشتی نسبتا غیر رسمی از مفهوم کلاس‌ها در برنامه‌نویسی شیء گرا تفسیر کرد. یک کلاس، سرشت نوعی از شیء را تعریف می کند – دارای تعدادی متد و خاصیت می باشد. به اشیائی که از کلاس ها ایجاد می شوند، نمونه‌های یک کلاس می گویند.

prototype ها برای تعریف خاصیت‌هایی مفید می باشند که مقدار مشابهی را در طول همه‌ی نمونه‌های یک کلاس به اشتراک می گذارند، مانند متدها. خاصیت‌هایی که برای هر نمونه متفاوت هستند، مانند خاصیت type در مثال خرگوش بایستی به طور مستقیم در خود اشیاء ذخیره بشوند.

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

function makeRabbit(type) {
  let rabbit = Object.create(protoRabbit);
  rabbit.type = type;
  return rabbit;
}

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

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

function Rabbit(type) {
  this.type = type;
}
Rabbit.prototype.speak = function(line) {
  console.log(`The ${this.type} rabbit says '${line}'`);
};

let weirdRabbit = new Rabbit("weird");

سازنده‌ها (در حقیقت همه توابع) به طور خودکار خاصیتی به نام prototype را می گیرند، که این خاصیت به صورت پیش فرض، یک شیء خالی را که خود از Object.prototype گرفته شده است، نگه داری می کند. اگر بخواهید می توانید این خاصیت را تغییر دهید و با شیءای دیگر عوض کنید. یا می توانید به شیء خالی فعلی خاصیت‌هایی را اضافه کنید همانطور که در مثال این کار انجام شد.

رسم است که نام سازنده‌ها با حروف بزرگ شروع شوند تا بتوان به سادگی بین آن ها و توابع معمولی تمایز قائل شد.

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

console.log(Object.getPrototypeOf(Rabbit) ==
            Function.prototype);
// → true
console.log(Object.getPrototypeOf(weirdRabbit) ==
            Rabbit.prototype);
// → true

استفاده از نماد class

بنابراین مفهوم کلاس‌های جاوااسکریپت، همان توابع سازنده به همراه یک خاصیت prototype می باشند. به همین صورت نیز کار می کنند و تا پیش از سال 2015، این روش تنها راه پیاده‌سازی بود. این روز‌ها، روش مناسب‌تری برای پیاده‌سازی کلاس‌ها در اختیار داریم.

class Rabbit {
  constructor(type) {
    this.type = type;
  }
  speak(line) {
    console.log(`The ${this.type} rabbit says '${line}'`);
  }
}

let killerRabbit = new Rabbit("killer");
let blackRabbit = new Rabbit("black");

کلیدواژه‌ی class موجب شروع یک اعلان کلاس می شود که به ما این امکان را می دهد تا سازنده‌ و مجموعه‌ی متدها را یکجا تعریف کنیم. هر تعداد متدی که نیاز باشد را می توان درون کروشه‌های تعریف کلاس قرار داد. متدی که با نام constructor نوشته می شود، به صورت خاصی تفسیر می شود. این متد تابع سازنده‌ی واقعی را فراهم می سازد که به نام Rabbit قید خواهد خورد. دیگر متدها درون prototype سازنده بسته‌بندی می شوند. بنابراین، تعریف کلاس به شکل بالا، معادل تعریف سازنده در قسمت قبل است. فقط زیباتر به نظر می رسد.

در تعریف کلاس فقط می توان متدها – خاصیت‌هایی که توابع را نگه‌داری می کنند – را برای اضافه شدن به prototype تعریف کرد. این محدودیت، در مواقعی که قصد دارید مقداری از نوع غیر تابع را ذخیره کنید، ممکن است مشکل ایجاد کند. برای این گونه خاصیت‌ها، می توانید همچنان به صورت مستقیم prototype را بعد از تعریف کلاس تغییر دهید.

class درست شبیه function می تواند به صورت عبارت و دستور استفاده شود. اگر به عنوان عبارت استفاده شود، متغیری تعریف نکرده و سازنده را به عنوان یک مقدار تولید می کند. می توانید نام کلاس را در این روش از تعریف حذف کنید.

let object = new class { getWord() { return "hello"; } };
console.log(object.getWord());
// → hello

بازنویسی و تغییر خاصیت‌های مشتق شده

هنگامی که خاصیتی را به یک شیء اضافه می کنید، فارغ از اینکه در prototype آن وجود داشته باشد یا خیر، خاصیت مورد نظر به خود شیء اضافه خواهد شد. در صورت وجود خاصیتی با همین نام در prototype، خاصیت موجود در prototype بی اثر خواهد بود و پشت خاصیت خود شیء پنهان می شود.

Rabbit.prototype.teeth = "small";
console.log(killerRabbit.teeth);
// → small
killerRabbit.teeth = "long, sharp, and bloody";
console.log(killerRabbit.teeth);
// → long, sharp, and bloody
console.log(blackRabbit.teeth);
// → small
console.log(Rabbit.prototype.teeth);
// → small

نمودار زیر شرایطی را به تصویر می کشد که بعد از اجرای کد بالا رخ می دهد. prototypeهای Rabbit و Object پشت killerRabbit قرار می گیرند، مانند نوعی پس‌زمینه، که در صورت نبودن خاصیت‌ها در خود شیء، به آن‌ها رجوع می شود.

Rabbit object prototype schema

تغییر و بازنویسی خاصیت‌هایی که در پروتوتایپ وجود دارند می تواند کاربرد داشته باشد. همانطور که در مثال rabbit teeth (خاصیت teeth در شیء خرگوش) نشان داده شد، می توان از آن برای مشخص کردن خاصیت های استثناء در نمونه‌ اشیاء یک کلاس عمومی‌تر استفاده کرد و اجازه داد اشیاء معمول، مقدار استاندارد را از prototype خود دریافت کنند.

بازنویسی به این شکل (overridding)، همچنین برای تعریف نسخه‌ی متفاوتی از متد toString برای prototypeهای استاندارد تابع(function) و آرایه (array) استفاده می شود. متدی که با toString پیش‌فرض شیء پایه متفاوت است.

console.log(Array.prototype.toString ==
            Object.prototype.toString);
// → false
console.log([1, 2].toString());
// → 1,2

فراخوانی toString بر روی یک آرایه نتیجه‌ای شبیه فراخوانی .join(",") روی آن را خواهد داشت – که باعث می شود بین مقادیر آرایه ویرگول قرار گیرد. فراخوانی مستقیم Object.prototype.toString با یک آرایه، رشته‌ی متفاوتی را تولید می کند. این تابع چیزی در مورد آرایه ها نمی داند، پس خیلی ساده واژه‌ی object و نام نوع داده را بین یک جفت براکت چاپ می کند.

console.log(Object.prototype.toString.call([1, 2]));
// → [object Array]

ساختار داده‌ی Map

با واژه‌ی map در فصل پیش آشنا شدیم. از آن برای تغییر یک ساختار داده به وسیله‌ی اعمال یک تابع به عناصر آن استفاده شد. درست است که شاید کمی گیج‌کننده باشد اما در برنامه‌نویسی، همین واژه برای موضوع مرتبط و نسبتا متفاوتی نیز استفاده می شود.

یک map یک ساختار داده است که مقدارهایی را (کلید‌ها) به مقدارهای دیگر مرتبط می سازد. به عنوان مثال، ممکن است بخواهید اسم‌ها را به سن‌ها نگاشت (map) کنید. می‌توان از یک شیء برای این کار استفاده کرد.

let ages = {
  Boris: 39,
  Liang: 22,
  Júlia: 62
};

console.log(`Júlia is ${ages["Júlia"]}`);
// → Júlia is 62
console.log("Is Jack's age known?", "Jack" in ages);
// → Is Jack's age known? false
console.log("Is toString's age known?", "toString" in ages);
// → Is toString's age known? true

در این مثال نام خاصیت های شیء، برابر با نام اشخاص و مقدار‌شان برابر با سن افراد تنظیم شده است. اما بی شک، در بین اسامی، کسی به نام toString نداشته‌ایم. بله به دلیل اینکه اشیاء ساده، از Object.prototype مشتق شده اند، به نظر می رسد که این خاصیت آنجا وجود دارد.

بدین لحاظ، استفاده از اشیاء ساده به جای map خطراتی دارد. راه های متفاوتی برای فرار از این مشکل وجود دارد. اول اینکه می توان شیءای را بدون prototype ایجاد کرد. اگر به متد Object.create مقدار null را بفرستید، شیء تولیدی دیگر از روی Object.prototype ساخته نمی شود و می توان با خیال راحت به عنوان یک نگاشت (map) از آن استفاده کرد.

console.log("toString" in Object.create(null));
// → false

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

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

let ages = new Map();
ages.set("Boris", 39);
ages.set("Liang", 22);
ages.set("Júlia", 62);

console.log(`Júlia is ${ages.get("Júlia")}`);
// → Júlia is 62
console.log("Is Jack's age known?", ages.has("Jack"));
// → Is Jack's age known? false
console.log(ages.has("toString"));
// → false

متدهای get و set و has بخش‌هایی از رابط شیء Map هستند. نوشتن ساختار داده‌ای که بتوان به وسیله‌ی آن مجموعه‌ی بزرگی از مقدارها را به سرعت به روز رسانی و جستجو کرد کار ساده ای نیست، اما نیازی نیست ما نگران آن باشیم. کسانی قبلا این کار را برای ما انجام داده اند و می توانیم به سراغ این رابط ساده برویم و از حاصل کار آن ها استفاده کنیم.

اگر شیء ساده‌ای دارید و بنا به دلایلی لازم است از آن به عنوان یک ساختار map استفاده کنید، لازم است بدانید که Object.keys فقط کلید‌های خود یک شیء را برمی گرداند نه آن‌هایی که در prototype آن قرار دارند. به عنوان یک جایگزین برای عملگر in، می توانید از متد hasOwnProperty استفاده کنید که پروتوتایپ شیء را در نظر نمی گیرد.

console.log({x: 1}.hasOwnProperty("x"));
// → true
console.log({x: 1}.hasOwnProperty("toString"));
// → false

چندریختی (Polymorphism)

زمانی که تابع String ( که یک مقدار را به رشته تبدیل می کند) را روی یک شیء فراخوانی می کنید، متد toString آن شیء فراخوانی می شود و سعی می کند تا رشته‌ای معنادار از شیء مورد نظر تولید کند. پیش‌تر اشاره کردم که بعضی از prototypeهای استاندارد، نسخه‌ی toString اختصاصی خودشان را تعریف می کنند تا با این کار بتوانند اطلاعات مفیدتری نسبت به "[object Object]" تولید کنند. شما نیز می توانید این کار را انجام دهید.

Rabbit.prototype.toString = function() {
  return `a ${this.type} rabbit`;
};

console.log(String(blackRabbit));
// → a black rabbit

این نمونه‌ای ساده از یک ایده‌ی قدرتمند است. زمانی که کدی نوشته می شود تا با اشیائی کار کند که دارای یک رابط خاص هستند – در این مثال، یک متد toString – می توان با این کد، هر شیء دیگری را که از این رابط پشتیبانی می کند، شامل نمود و به درستی استفاده کرد.

این تکنیک را چندریختی می گویند. یک کد چندریخت می تواند با مقدارهایی از شکل‌های مختلف کار کنند مادامیکه این شکل‌ها رابطی که کد انتظارش را دارد پشتیبانی کند.

در فصل 4 اشاره کردم که با استفاده از حلقه‌ی for/of می توان ساختارهای داده‌ی مختلف را پیمایش کرد. این یک مورد دیگر از چندریختی محسوب می شود – این گونه حلقه‌ها از ساختار داده انتظار دارند که رابط‌ خاصی را در دسترس حلقه قرار دهند، رابطی که آرایه‌ها و رشته ها فراهم می سازند. و شما نیز می توانید این رابط را به اشیاء خودتان اضافه کنید! اما قبل از اینکه بتوانیم این کار را بکنیم، لازم است بدانیم که سمبل (symbol) چیست.

سمبل‌ها (Symbols)

می توان در چند رابط، نام خاصیت‌ یکسانی برای کارهای مختلف استفاده کرد. به عنوان مثال، من می توانم رابطی تعریف کنم که در آن متد toString به صورت فرضی، یک شیء را به یک تکه ریسمان تبدیل کند. اما یک شیء نمی تواند هم از رابطی که تعریف کردیم و هم پیاده‌سازی استاندارد toString مطابقت کند.

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

پیش‌تر که ادعا کردم نام خاصیت‌ها از جنس رشته هستند، عبارت کاملا دقیقی استفاده نکرده بودم. بله معمولا از جنس رشته‌اند اما می توانند از نوع symbol نیز باشند. سمبل‌ها مقادیری هستند که با تابع Symbol ایجاد می شوند. برخلاف رشته‌ها، سمبل‌هایی که تازه ایجاد می شوند یکتا هستند – نمی توان یک سمبل یکسان را دوبار ایجاد کرد.

let sym = Symbol("name");
console.log(sym == Symbol("name"));
// → false
Rabbit.prototype[sym] = 55;
console.log(blackRabbit[sym]);
// → 55

رشته‌ای که به تابع Symbol ارسال می کنید در هنگام تبدیل سمبل به رشته استفاده می شود و می تواند شناسایی آن را مثلا هنگام نشان دادن در کنسول ساده تر کند. فارغ از آن معنای دیگری ندارد - می توان چندین سمبل را با یک نام تعریف کرد.

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

const toStringSymbol = Symbol("toString");
Array.prototype[toStringSymbol] = function() {
  return `${this.length} cm of blue yarn`;
};

console.log([1, 2].toString());
// → 1,2
console.log([1, 2][toStringSymbol]());
// → 2 cm of blue yarn

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

let stringObject = {
  [toStringSymbol]() { return "a jute rope"; }
};
console.log(stringObject[toStringSymbol]());
// → a jute rope

رابط تکرارکننده (Iterator Interface)

انتظار می رود شیءای که به حلقه‌ی for/of داده می شود قابل تکرار باشد. یعنی دارای متدی باشد که به وسیله‌ی Symbol.iterator نام گذاری شده است ( مقداری از نوع سمبل که توسط خود زبان تعریف شده است و به عنوان یک خاصیت از تابع Symbol ذخیره می شود).

وقتی این متد فراخوانی می شود، خروجی آن یک شیء خواهد بود که رابط دومی را فراهم می سازد، رابط تکرارکننده، رابطی که عمل تکرار را انجام می دهد. این تکرارکننده دارای متدی به نام next است که نتیجه‌ی بعدی را برمی گرداند. این نتیجه باید یک شیء با خاصیتی به نام value باشد که مقدار بعدی را در صورت وجود در دسترس قرار می دهد و خاصیت دیگری به نام done دارد که در صورت نبود نتیجه‌ای دیگر، برابر با true خواهد و در غیر این صورت مقدار false را خواهد داشت.

توجه داشته باشید که نام خاصیت های next، value و done از نوع رشته‌ی ساده است نه از جنس سمبل. فقط Symbol.iterator است که در واقع از جنس سمبل است و احتمالا به اشیاء متفاوت زیادی اضافه خواهد شد.

می توانیم مستقیما از این رابط استفاده کنیم.

let okIterator = "OK"[Symbol.iterator]();
console.log(okIterator.next());
// → {value: "O", done: false}
console.log(okIterator.next());
// → {value: "K", done: false}
console.log(okIterator.next());
// → {value: undefined, done: true}

بیایید یک ساختار قابل تکرار را پیاده‌سازی کنیم. در مثال زیر یک کلاس ماتریس خواهیم ساخت که مانند یک آرایه‌ی دوبعدی عمل می کند

class Matrix {
  constructor(width, height, element = (x, y) => undefined) {
    this.width = width;
    this.height = height;
    this.content = [];

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        this.content[y * width + x] = element(x, y);
      }
    }
  }

  get(x, y) {
    return this.content[y * this.width + x];
  }
  set(x, y, value) {
    this.content[y * this.width + x] = value;
  }
}

کلاس بالا محتوای خود را در یک آرایه به تعداد عناصر width × height ذخیره می کند. عناصر به صورت ردیف به ردیف ذخیره می شوند، بنابراین به عنوان مثلا عنصر سوم در ردیف پنجم در موقعیت 4 × width + 2 ذخیره می شود ( با در نظر داشتن اندیس گذاری از صفر).

تابع سازنده، یک طول، یک عرض و تابعی اختیاری برای محتوا می گیرد که این تابع برای مقداردهی اولیه استفاده می شود. متدهای get و set برای به روز رسانی عناصر و دریافت آن ها در ماتریس تعریف شده اند.

در زمان پیمایش یک ماتریس، معمولا دانستن موقعیت عناصر به اندازه‌ی خود عناصر مهم هستند، بنابراین تکرارکننده‌ی ما، اشیائی با خاصیت‌های x و y و value تولید می کند.

class MatrixIterator {
  constructor(matrix) {
    this.x = 0;
    this.y = 0;
    this.matrix = matrix;
  }

  next() {
    if (this.y == this.matrix.height) return {done: true};

    let value = {x: this.x,
                 y: this.y,
                 value: this.matrix.get(this.x, this.y)};
    this.x++;
    if (this.x == this.matrix.width) {
      this.x = 0;
      this.y++;
    }
    return {value, done: false};
  }
}

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

بیایید کلاس Matrix را قابل تکرار کنیم. در این کتاب، گاهی بعد از تعریف کلاس‌ها، prototype را دستکاری خواهم کرد تا متدهایی را به آن‌ها اضافه کنم، در نتیجه کدها مجزا و کوچک خواهند ماند و به دیگر قسمت‌ها وابسته نخواهند شد. در یک برنامه‌ی معمولی، جایی که نیازی نیست تا کدها را به قسمت‌های کوچکتر تقسیم کرد، می توانید این متدها را مستقیما درون بدنه کلاس تعریف کنید.

Matrix.prototype[Symbol.iterator] = function() {
  return new MatrixIterator(this);
};

اکنون می توانیم یک ماتریس را به وسیله‌ی for/of پیمایش کنیم.

let matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`);
for (let {x, y, value} of matrix) {
  console.log(x, y, value);
}
// → 0 0 value 0,0
// → 1 0 value 1,0
// → 0 1 value 0,1
// → 1 1 value 1,1

گیرنده‌ها(getters) ، گذارنده‌ها(setters) و متدهای ایستا (static)

رابط‌ها بیشتر از متد‌ها تشکیل شده اند، اما می توانند خاصیت‌هایی که مقادیر تابعی را نگه داری نمی کنند را نیز داشته باشند. به عنوان مثال، اشیاء Map دارای خاصیتی به نام size می باشند که تعداد کلید‌هایی که در آن‌ها ذخیره شده است را نگه داری می کند.

در این‌گونه اشیاء لزومی ندارد خاصیتی مثل size را مستقیما در خود نمونه شیء محاسبه و ذخیره نمود. حتی خاصیت‌هایی که به صورت مستقیم در دسترس هستند، ممکن است متدی را مخفیانه فراخوانی کنند. این گونه‌ی خاصیت‌ها را getter یا گیرنده‌ی می گویند که به وسیله‌ی نوشتن get در ابتدای نام یک متد، در یک عبارت تعریف شیء یا کلاس، تعریف می شوند.

let varyingSize = {
  get size() {
    return Math.floor(Math.random() * 100);
  }
};

console.log(varyingSize.size);
// → 73
console.log(varyingSize.size);
// → 49

زمانی که کسی مقدار خاصیت‌ size را درخواست می کند، متدی که به آن پیوند خورده است فراخوانی می شود. می توانید کار مشابهی را برای مقدار دهی به یک خاصیت هم انجام دهید که به آن setter یا گذارنده می گویند.

class Temperature {
  constructor(celsius) {
    this.celsius = celsius;
  }
  get fahrenheit() {
    return this.celsius * 1.8 + 32;
  }
  set fahrenheit(value) {
    this.celsius = (value - 32) / 1.8;
  }

  static fromFahrenheit(value) {
    return new Temperature((value - 32) / 1.8);
  }
}

let temp = new Temperature(22);
console.log(temp.fahrenheit);
// → 71.6
temp.fahrenheit = 86;
console.log(temp.celsius);
// → 30

کلاس Temperature در مثال بالا این امکان را فراهم می کند تا میزان دما را به صورت سلسیوس یا فارنهایت بنویسید، اما در درون کلاس، این مقدار فقط به سلسیوس ذخیره می شود و به طور خودکار توسط متدهای گیرنده و گذارنده (setter, getter) به ‍fahrenheit تبدیل می شود.

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

درون تعریف یک کلاس، متدهایی که کلیدواژه‌ی static در ابتدای آن ها نوشته می شود، روی تابع سازنده ذخیره می شوند. بنابراین در کلاس Temperature می توانید برای تولید دما به وسیله‌ی درجه‌ی فارنهایت ازTemperature.fromFahrenheit(100) استفاده کنید.

ارث‌بری

بعضی از ماتریس‌ها را به عنوان ماتریس‌های متقارن می شناسند. اگر یک ماتریس متقارن را حول قطر بالا-چپ-به-پایین-راست بازتاب کنید، تفاوتی در شکل آن ایجاد نمی شود. به بیان دیگر، مقدار موجود در x,y همیشه مشابه مقدار ‌y,x است.

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

سیستم prototype در جاوااسکریپت این امکان را فراهم کرده است که یک کلاس جدید را بر اساس یک کلاس دیگر اما با بازتعریف بعضی از خاصیت‌های آن ایجاد کنیم. prototype کلاس جدید از prototype کلاس قبلی مشتق می شود اما تعریف جدیدی را برای متد set آن به عنوان مثال در نظر می گیرد.

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

class SymmetricMatrix extends Matrix {
  constructor(size, element = (x, y) => undefined) {
    super(size, size, (x, y) => {
      if (x < y) return element(y, x);
      else return element(x, y);
    });
  }

  set(x, y, value) {
    super.set(x, y, value);
    if (x != y) {
      super.set(y, x, value);
    }
  }
}

let matrix = new SymmetricMatrix(5, (x, y) => `${x},${y}`);
console.log(matrix.get(2, 3));
// → 3,2

استفاده از واژه‌ی extends به این معنا است که این کلاس نباید بر اساس prototype پیش‌فرض Object ساخته شود بلکه بر اساس کلاس دیگری خواهد بود. این کلاس را superclass (کلاس والد) می نامند. کلاسی که از آن گرفته می شود را subclass (زیرکلاس) می گویند.

برای مقداردهی اولیه یک نمونه از SymmetricMatrix، سازنده، تابع سازنده‌ی کلاس والدش (superclass) را به وسیله‌ کلیدواژه‌ی super فراخوانی می کند. این کار لازم است به این دلیل که اگر این شیء جدید قرار است شبیه Matrix (به‌طورکلی) رفتار کند، به خاصیت‌هایی که ماتریس‌ها دارند نیاز پیدا خواهد کرد. برای اطمینان از متقارن بودن ماتریس، تابع سازنده، متد element را در بر می گیرد تا مختصات را برای مقادیر پایین قطر اصلی جابجا کند.

متد set دوباره از super استفاده می کند، اما این بار هدف، فراخوانی سازنده‌اش نیست. بلکه برای فراخوانی یک متد خاص از متدهای کلاس والد (superclass) می باشد. متد set را بازنویسی می کنیم اما می خواهیم از رفتار اصلی آن استفاده کنیم. به دلیل اینکه this.set به متد set جدید اشاره می کند، نمی توان از آن استفاده کرد. درون متد‌های کلاس، کلیدواژه‌ی super راهی فراهم می سازد تا متد‌هایی که در کلاس والد تعریف شده اند را بتوان فراخوانی کرد.

با کمک ارث‌بری می توانیم با کار نسبتا کمتری، نوع‌ داده‌های متفاوتی از انواع داده‌ی موجود بسازیم. درکنار کپسوله‌سازی و چندریختی، ارث‌بری یکی از مفاهیم اساسی برنامه‌نویسی شیء گرا می باشد. البته دو ویژگی اول را عموما به عنوان ایده‌هایی فوق‌العاده می شناسند اما در باره‌ی ارث‌بری اختلاف‌ نظرهایی وجود دارد.

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

عملگر instanceof

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

console.log(
  new SymmetricMatrix(2) instanceof SymmetricMatrix);
// → true
console.log(new SymmetricMatrix(2) instanceof Matrix);
// → true
console.log(new Matrix(2, 2) instanceof SymmetricMatrix);
// → false
console.log([1] instanceof Array);
// → true

این عملگر انواع وارث را مورد کنکاش قرار می دهد، مثلا SymmetricMatrix نمونه‌ای از Matrix است. این عملگر را همچنین می توان برای سازنده‌های استاندارد مثل Array نیز استفاده کرد. تقریبا همه‌ی اشیاء نمونه‌ای از Object هستند.

خلاصه

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

می توان از توابع سازنده که معمولا نامشان با حروف بزرگ شروع می شود، با استفاده از کلیدواژه‌ی new، برای ایجاد اشیاء جدید استفاده کرد. prototype شیء ایجاد شده، شیءای است که در خاصیت‌ prototype سازنده پیدا می شود. می توان از این ویژگی برای قرار دادن همه‌ی خاصیت‌های مشترک یک نوع خاص در prototype آن بهره برد. روش دیگری برای تعریف یک سازنده و prototype آن وجود دارد که از کلیدواژه‌ی class استفاده می کند.

می توانید با تعریف گذارنده‌ها (setters) و گیرنده‌ها (getters)، به طور مخفیانه متدهایی را ایجاد کنید که با هر بار دسترسی به یک خاصیت شیء، فراخوانی می شوند. متدهای ایستا (static) متدهایی هستند که در سازنده‌ی کلاس ذخیره می شوند نه در prototype آن.

عملگر instanceof را اگر به یک شیء و یک سازنده اعمال کنید، به شما خواهد گفت که آن شیء نمونه‌ای از آن سازنده می باشد یا خیر.

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

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

زمانی که چندین کلاس را پیاده سازی می کنیم که تنها در بعضی جزئیات با هم تفاوت دارند، می توانیم کلاس‌های جدید را به عنوان زیرکلاس‌های کلاس های موجود بنویسیم که بعضی از رفتارهای آن ها را به ارث ببرند.

تمرین‌ها

تعریف یک نوع بردار

کلاسی به نام Vec تعریف کنید که نشان‌دهنده‌ی یک بردار در فضای دوبعدی باشد. این کلاس دو عدد x و y را به عنوان پارامتر (عددی) دریافت می کند، که با همین نام‌ها به عنوان خاصیت ذخیره می شوند.

به پروتوتایپ Vec دو متد plus و minus را اضافه کنید که بردار دیگری را به عنوان یک پارامتر گرفته و بردار جدیدی را برمی‌گرداند، برداری که تفاوت یا مجموع مقدارهای x و y دو بردار (this و پارامترها) را باز می گرداند.

خاصیت گیرنده‌ای (getter) به نام length به prototype اضافه کنید که طول بردار را محاسبه می کند – فاصله‌ی بین نقطه‌ی (x,y) از مبدا (0,0).

// Your code here.

console.log(new Vec(1, 2).plus(new Vec(2, 3)));
// → Vec{x: 3, y: 5}
console.log(new Vec(1, 2).minus(new Vec(2, 3)));
// → Vec{x: -1, y: -1}
console.log(new Vec(3, 4).length);
// → 5

اگر تعریف کلاس را فراموش کرده اید، به مثال کلاس Rabbit مراجعه کنید.

افزودن یک خاصیت گیرنده به سازنده را می توان با قراردادن واژه‌ی get در ابتدای نام متد انجام داد. برای محاسبه‌ی فاصله‌ی بین نقاط (0,0) و (x, y)، می توانید از قضیه‌ی فیثاغورث استفاده کنید که در آن توان‌دوم فاصله‌‌ای که مورد نظر ما است (وتر) برابر است با مجموع توان‌های دوم اندازه مختصات x و y. بنابراین، √(x2 + y2) عددی است که شما لازم دارید. متد Math.sqrt، برای محاسبه‌ی ریشه‌ی دوم یک عدد در جاوااسکریپت استفاده می شود.

گروه‌ها

محیط استاندارد جاوااسکریپت ساختار داده‌ی دیگری به نام Set را فراهم می کند. مانند یک نمونه ازMap، یک set (مجموعه) مجموعه‌ای از مقدارها را نگه داری می کند. برخلاف Map، این ساختار داده مقادیر را با هم مرتبط نمی کند – فقط مشخص می کند که کدام مقادیر در مجموعه وجود دارند. یک مقدار فقط می تواند یکبار به مجموعه اضافه شود – اگر دوباره یک مقدار را اضافه کنیم، اثری نخواهد داشت.

کلاسی به نام Group (زیرا Set قبلا رزرو شده است) تعریف کنید. شبیه Set، این کلاس متدهای add، delete و has را دارد. سازنده‌ی این کلاس یک گروه (group) خالی ایجاد می کند، متد add، یک مقدار را به گروه اضافه می کند (البته اگر قبلا عضو گروه نبود)، متد delete آرگومان ورودی‌اش را از گروه حذف می کند (البته اگر وجود داشت)، و متد has، یک مقدار بولی برمی‌گرداند که نشان می دهد آرگومانش در مجموعه وجود دارد یا خیر.

برای محاسبه‌ی تشابه‌ دو مقدار ، از عملگر === یا چیزی مشابه مانند indexOf، استفاده کنید.

به کلاس متد استاتیکی به نام from اضافه کنید که شیءای قابل شمارش را به عنوان آرگومان دریافت می کند و گروهی ایجاد می کند که دارای تمامی مقادیری است که با پیمایش شیء بدست می آید.

class Group {
  // Your code here.
}

let group = Group.from([10, 20]);
console.log(group.has(10));
// → true
console.log(group.has(30));
// → false
group.add(10);
group.delete(10);
console.log(group.has(10));
// → false

آسان‌ترین روش انجام این تمرین این است که آرایه‌ای از اعضای گروه را در یک خاصیت ذخیره کنید. متدهای ‍includes یا indexOf را می توان برای بررسی وجود یک مقدار در آرایه استفاده کرد.

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

حذف یک عنصر از یک آرایه به وسیله‌ی دستور delete نیاز به ملاحظات بیشتری دارد. به جای آن می توانید از متد filter برای ایجاد آرایه‌ای جدید استفاده کنید که عنصر مورد نظر را ندارد. فراموش نکنید که خاصیتی که اعضای گروه را نگه‌داری می کند با آرایه‌ی جدید به‌روز شود.

متد from می تواند از یک حلقه‌ی for/of برای گرفتن مقدارها از یک شیء قابل تکرار استفاده کند و با فراخوانی add آن ها را درون گروه تازه ایجاد شده قرار دهد.

گروه‌های قابل تکرار

کلاس Group را که در مثال قبل ایجاد کردید اکنون قابل تکرار کنید. اگر در مورد شکل و ساختار رابط سوال دارید، می توانید به بخشی که پیش‌تر در همین فصل درباره‌ی رابط تکرارکننده آمد مراجعه نمایید.

اگر از یک آرایه برای نمایش اعضای گروه استفاده کرده اید، فقط به بازگرداندن تکرارکننده‌ای که با فراخوانی متد Symbol.iterator روی آرایه ایجاد شده است اکتفا نکنید. این روش کار خواهد کرد اما شما را از هدف این تمرین منحرف می کند.

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

// Your code here (and the code from the previous exercise)

for (let value of Group.from(["a", "b", "c"])) {
  console.log(value);
}
// → a
// → b
// → c

احتمالا ارزشش را دارد که کلاس جدیدی به نام GroupIterator تعریف شود. نمونه‌های تکرارکننده باید خاصیتی برای رصد موقعیت فعلی در گروه داشته باشند. هر بار که next فراخوانی می شود، این تابع رسیدن به پایان را بررسی می کند که در غیر این صورت، از مقدار فعلی عبور کرده و آن را بر می گرداند.

کلاس Group خودش متدی به نام Symbol.iterator می گیرد که در صورت فراخوانی، نمونه‌ای جدید از کلاس تکرارکننده را برای آن گروه برمی‌گرداند.

قرض گرفتن یک متد

پیش تر در این فصل گفته شد که متد hasOwnProperty را می توان به عنوان جایگزینی کاراتر نسبت به عملگر in استفاده کرد البته در شرایطی که قصد دارید تا خاصیت‌های prototype را درنظر نگیرید. اما اگر بخواهید در شیء نگاشت‌تان (map) کلید "hasOwnProperty" را داشته باشید چه؟ در این صورت دیگر نمی توانید آن متد را فراخوانی کنید چراکه خاصیت خود شیء باعث می شود که آن مخفی شود.

آیا می توانید راهی پیدا کنید که بتوان hasOwnProperty را روی یک شی فراخونی کرد، شیءای که خاصیتی با همین نام دارد؟

let map = {one: true, two: true, hasOwnProperty: true};

// Fix this call
console.log(map.hasOwnProperty("one"));
// → true

به یاد داشته باشید که متدهایی که در یک شیء معمولی وجود دارند از Object.prototype می آیند.

همچنین در نظر داشته باشید که می توانید یک تابع را با یک this خاص به وسیله‌ی متد call توابع، فراخوانی کنید.