شاید بپرسید: اصلاً چرا باید خودمان را به دردسر بیندازیم و از متدهای getRed و setRed استفاده کنیم، وقتی میتوانیم خیلی راحت و مستقیم به آرایهی values در نمونه (Instance) دسترسی داشته باشیم؟
class Color {
constructor(r, g, b) {
this.values = [r, g, b];
}
}
const red = new Color(255, 0, 0);
red.values[0] = 0;
console.log(red.values[0]); // 0
در برنامهنویسی شیءگرا فلسفهای به نام «کپسولهسازی» (Encapsulation) وجود دارد. این اصول به ما میگوید که شما نباید به لایه و ساختار پیادهسازیِ زیرین یک Object دسترسی مستقیم داشته باشید، بلکه باید از متدهای انتزاعی و استاندارد برای تعامل با آن استفاده کنید.
برای مثال، فرض کنید یکباره تصمیم بگیریم مدل ذخیرهسازی رنگها را در بدنه کلاس از RGB به HSL تغییر دهیم:
class Color {
constructor(r, g, b) {
// ویژگی values اکنون یک آرایه HSL است!
this.values = rgbToHSL([r, g, b]);
}
getRed() {
return hslToRGB(this.values)[0];
}
setRed(value) {
const rgb = hslToRGB(this.values);
rgb[0] = value;
this.values = rgbToHSL(rgb);
}
}
const red = new Color(255, 0, 0);
console.log(red.values[0]); // 0 -> دیگر مقدار 255 را نمیدهد، چون مقدار H برای رنگ قرمز خالص 0 است!
با این تغییر ساختار، تصور کاربر مبنی بر اینکه values همیشه حاوی مقادیر RGB است کاملاً به هم میریزد و ممکن است منطق برنامهاش با خطا مواجه شود. بنابراین، اگر شما توسعهدهندهی یک کلاس هستید، بهتر است ساختار دادههای داخلی نمونهی خود را از دید کاربر پنهان کنید؛ این کار هم API شما را تمیز نگه میدارد و هم مانع از خراب شدن کدهای کاربر در هنگام بازنویسیها و ریفکتورهای (Refactors) داخلی و بیخطر شما میشود. در کلاسهای جاوااسکریپت، این وظیفه بر عهدهی فیلدهای خصوصی (Private fields) است.
یک فیلد خصوصی (Private field)، شناسهای است که با علامت هشتگ (#) شروع میشود. این علامت بخشی جداییناپذیر از نام فیلد است؛ به این معنی که یک فیلد خصوصی هرگز با یک فیلد یا متد عمومی (Public) همنام خود تداخل پیدا نمیکند.
برای اینکه بتوانید در هر جای کلاس به یک فیلد خصوصی ارجاع دهید، حتماً باید ابتدا آن را در بدنه کلاس اعلان (Declare) کنید (نمیتوانید یک المان خصوصی را به صورت آنی و در لحظه بسازید). به جز این مورد، فیلد خصوصی عملکردی کاملاً مشابه یک ویژگی معمولی دارد:
class Color {
// اعلان فیلد: هر نمونه از کلاس Color یک فیلد خصوصی به نام values# دارد.
#values;
constructor(r, g, b) {
this.#values = [r, g, b];
}
getRed() {
return this.#values[0];
}
setRed(value) {
this.#values[0] = value;
}
}
const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255
تلاش برای دسترسی به فیلدهای خصوصی در خارج از کلاس، فوراً منجر به خطای ساختاری (SyntaxError) در همان ابتدای کار میشود:
console.log(red.#values);
// خطای سینتکس:
// SyntaxError: Private field '#values' must be declared in an enclosing class
نکته: کدهایی که مستقیماً در کنسول مرورگر کروم اجرا میشوند، میتوانند به المانهای خصوصی خارج از کلاس دسترسی داشته باشند. این صرفاً یک قابلیت تسهیلکننده و استثنا در DevTools مرورگر برای راحتی کار برنامهنویسان است و جزو قوانین زبان جاوااسکریپت نیست.
فیلدهای خصوصی در جاوااسکریپت به شدت سختگیرانه (Hard private) هستند؛ اگر کلاس شما متدی برای اکسپوز کردن و بیرون دادن این فیلدها نداشته باشد، هیچ مکانیزمی برای بازیابی آنها از بیرون کلاس وجود ندارد. این ویژگی دست شما را برای هرگونه بازنویسی و Refactor در فیلدهای خصوصی کلاس باز میگذارد، به شرطی که رفتار متدهای عمومی تغییر نکند.
حالا که فیلد values را خصوصی کردهایم، میتوانیم به جای متدهای ساده، منطق و اعتبارسنجیهای مهمی را به setRed اضافه کنیم؛ مثلاً بررسی کنیم که مقدار ورودی یک R value معتبر باشد:
class Color {
#values;
constructor(r, g, b) {
this.#values = [r, g, b];
}
getRed() {
return this.#values[0];
}
setRed(value) {
// اعتبارسنجی مقدار ورودی
if (value < 0 || value > 255) {
throw new RangeError("Invalid R value");
}
this.#values[0] = value;
}
}
const red = new Color(255, 0, 0);
red.setRed(1000); // خطای محدوده: RangeError: Invalid R value
اگر ویژگی values را عمومی رها میکردیم، کاربران میتوانستند به راحتی با تغییر مستقیم [values[0 این فیلتر را دور بزنند و رنگهای نامعتبر تولید کنند. اما با یک API به خوبی کپسولهشده، کدهای ما بسیار پایدارتر خواهند بود.
یک متد کلاس میتواند فیلدهای خصوصیِ نمونههای دیگر را هم بخواند، به شرطی که هر دو نمونه متعلق به یک کلاس باشند:
class Color {
#values;
constructor(r, g, b) {
this.#values = [r, g, b];
}
redDifference(anotherColor) {
// نیازی نیست values# حتماً از طریق this خوانده شود؛
// شما میتوانید به فیلدهای خصوصی سایر نمونههای همین کلاس نیز دسترسی داشته باشید.
return this.#values[0] - anotherColor.#values[0];
}
}
const red = new Color(255, 0, 0);
const crimson = new Color(220, 20, 60);
red.redDifference(crimson); // 35
اما اگر anotherColor نمونهای از کلاس Color نباشد، فیلد values# روی آن وجود نخواهد داشت (حتی اگر کلاس دیگری فیلد خصوصی همنامی به اسم values# داشته باشد، به چیز متفاوتی اشاره دارد و اینجا قابل دسترسی نیست). دسترسی به یک المان خصوصی ناموجود برخلاف ویژگیهای معمولی که undefined میدهند، باعث پرتاب خطا (Error) میشود.
اگر مطمئن نیستید که یک فیلد خصوصی روی یک شیء وجود دارد یا نه و میخواهید بدون استفاده از بلوک try/catch به آن دسترسی پیدا کنید، میتوانید از اپراتور in استفاده کنید:
class Color {
#values;
constructor(r, g, b) {
this.#values = [r, g, b];
}
redDifference(anotherColor) {
if (!(#values in anotherColor)) {
throw new TypeError("Color instance expected");
}
return this.#values[0] - anotherColor.#values[0];
}
}
نکته: به یاد داشته باشید که هشتگ (
#) یک نحوِ شناسه ویژه است و نمیتوانید با نام فیلد مثل یک رشته رفتار کنید. نوشتن"values" in anotherColorبه دنبال یک Property عمومی میگردد که نام آن دقیقاً رشتهی"values#"باشد، نه فیلد خصوصی.
استفاده از المانهای خصوصی محدودیتهایی نیز دارد: شما نمیتوانید یک نام را دو بار در یک کلاس اعلان کنید و همچنین امکان پاک کردن آنها با دستور delete وجود ندارد. هر دو کار منجر به خطای ساختاری اولیه (Early syntax error) میشوند:
class BadIdeas {
#firstName;
#firstName; // خطای سینتکس در این خط رخ میدهد
#lastName;
constructor() {
delete this.#lastName; // این هم خطای سینتکس است
}
}
علاوه بر فیلدها، متدها، گترها و سترها نیز میتوانند خصوصی باشند. این قابلیت زمانی بسیار مفید است که کار پیچیدهای دارید که کلاس باید به صورت داخلی آن را انجام دهد، اما هیچ بخش دیگری از برنامه نباید اجازه صدا زدن آن را داشته باشد.
برای مثال، ساخت یک المان سفارشی HTML (HTML custom element) را در نظر بگیرید که قرار است هنگام کلیک یا فعالشدن، کار پیچیدهای انجام دهد. این رفتار پیچیده باید کاملاً به این کلاس محدود شود، چون هیچ بخش دیگری از جاوااسکریپت قرار نیست (و نباید) به آن دسترسی داشته باشد:
class Counter extends HTMLElement {
#xValue = 0;
constructor() {
super();
this.onclick = this.#clicked.bind(this);
}
get #x() {
return this.#xValue;
}
set #x(value) {
this.#xValue = value;
window.requestAnimationFrame(this.#render.bind(this));
}
#clicked() {
this.#x++;
}
#render() {
this.textContent = this.#x.toString();
}
connectedCallback() {
this.#render();
}
}
customElements.define("num-counter", Counter);
در این نمونه، تقریباً تمام فیلدها و متدها برای کلاس خصوصی هستند. به این ترتیب، این کلاس رابط کاربری و Interface تمیزی را به بقیه کدهای برنامه ارائه میدهد که دقیقاً مانند یک المان پیشفرض و داخلیِ HTML عمل میکند و هیچ بخش دیگری از برنامه قدرت دستکاریِ کدهای داخلی Counter را ندارد.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://developer.mozilla.org/en-US/docs/Web/JavaScript