شما یک باگ را در سه فایل ردیابی میکنید، فقط برای اینکه بفهمید یک تابع کمکی بیضرر، نام یک کاربر را تغییر داده است. شما هرگز به فایل اصلی دست نزده بودید. یا حداقل اینطور فکر میکردید. در JavaScript، انتساب یک متغیر به متغیر دیگر همیشه آن چیزی نیست که انتظار دارید. این زبان دادههای خود را به دو استراتژی ذخیرهسازی تقسیم میکند، و فراموش کردن اینکه از کدام یک استفاده میکنید، همان روشی است که تغییرات (mutations) بیصدا به کد تولید (production) نفوذ میکنند.
برای جلوگیری از این اتفاق، باید تفاوت بین مقادیر اولیه (primitive values) و اشیاء (objects) را دقیقاً همانطور که موتور جاوااسکریپت میبیند، درک کنید.
مقادیر اولیه (Primitives): کپیهای واقعی
یک مقدار اولیه (primitive)، یک دادهی واحد و تجزیهناپذیر است. این مقدار را نمیتوان به مقادیر کوچکتر JavaScript تجزیه کرد. این زبان هفت نوع اولیه را تعریف میکند: String، Number، Boolean، Undefined، Null، Symbol و BigInt.
از آنجایی که مقادیر اولیه اتمیک (atomic) هستند، معمولاً مستقیماً درون اتصال متغیر (variable binding) قرار میگیرند. وقتی یک متغیر اولیه را در متغیر دیگری کپی میکنید، موتور دادههای واقعی را تکثیر میکند. هر متغیر جایگاه مستقل خود را در حافظه دریافت میکند.
let a = "Alina";
let b = a;
b = "Ali";
console.log(a); // "Alina"
console.log(b); // "Ali"
در اینجا، a دستنخورده باقی میماند. انتساب مجدد به b یک مقدار کاملاً جدید ایجاد کرد و b را به آن اشاره داد، در حالی که a هنوز رشتهی اصلی خود را نگه داشته است. این رفتار «کپی بر اساس مقدار» (copy by value) است. این موضوع برای اعداد، بولینها، نمادها (symbols) و بقیه خانوادهی مقادیر اولیه به همین صورت عمل میکند. شما میتوانید آنها را به توابع پاس دهید، دوباره انتساب دهید یا آنها را برگردانید، بدون اینکه نگران اثرات جانبی (side effects) روی متغیرهای همرده باشید.
اشیاء (Objects): آدرسهای مشترک
اشیاء متفاوت هستند. یک شیء، یک ظرف ترکیبی است که چندین قطعه داده را در کنار هم گروهبندی میکند. این دسته شامل اشیاء ساده (plain objects)، آرایهها، توابع، تاریخها و هر نوع غیر اولیه دیگری است. از آنجایی که این ساختارها میتوانند بزرگ و تودرتو شوند، JavaScript کل شیء را درون متغیر ذخیره نمیکند. در عوض، متغیر یک ارجاع (reference) را نگه میدارد؛ در واقع یک آدرس حافظه که به دادههای واقعی ذخیره شده در جای دیگر اشاره میکند.
وقتی یک شیء را به یک متغیر جدید انتساب میدهید، موتور آدرس را کپی میکند، نه خود شیء را. اکنون دو متغیر به دقیقاً یک خانه اشاره میکنند.
const user = { name: "Alina" };
const copy = user;
copy.name = "Ali";
console.log(user.name); // "Ali"
تغییر دادن copy.name باعث تغییر user.name نیز شد، زیرا هر دو نام به یک شیء زیربنایی یکسان ختم میشوند. این «کپی بر اساس ارجاع» (copy by reference) است. همین غافلگیری در مورد آرایهها نیز ظاهر میشود:
const scores = [82, 91, 74];
const backup = scores;
backup.push(88);
console.log(scores); // [82, 91, 74, 88]
تنها یک آرایه در حافظه وجود دارد. scores و backup صرفاً دو تابلوی راهنما هستند که به آن اشاره میکنند.
کلمه کلیدی const لایهای از سردرگمی را اضافه میکند. تعریف یک شیء با const اتصال متغیر را قفل میکند، به طوری که نمیتوانید آن را به آدرس جدیدی اشاره دهید. اما خودِ شیء را قفل نمیکند.
const settings = { theme: "dark" };
settings.theme = "light"; // Works perfectly.
settings = { theme: "dark" }; // TypeError
توسعهدهندگان اغلب انتظار دارند const وعدهی تغییرناپذیری (immutability) را بدهد. اما اینطور نیست. این کلمه فقط از انتساب مجددِ ارجاع جلوگیری میکند. اگر میخواهید محتویات تغییرناپذیر باشند، باید با هدف مشخص (copy with intent) کپیسازی کنید.
جایی که باگهای واقعی پنهان میشوند
باگهای مربوط به ارجاع (reference bugs) به ندرت به صورت انتسابهای متغیرِ آشکار ظاهر میشوند. آنها درون فراخوانیهای تابع پنهان میشوند.
function addTimestamp(record) {
record.timestamp = Date.now();
return record;
}
const original = { id: 1 };
addTimestamp(original);
console.log(original.timestamp); // A number now exists here. Oops.
پارامتر record کپیای از ارجاع را دریافت کرد. هرگونه تغییر در ویژگیها (property mutation) در داخل تابع، مستقیماً در شیءِ فراخواننده (caller) نوشته شد. تابع شبیه به یک تبدیل ساده به نظر میرسید، اما وضعیت (state) را از مرزهای محدوده (scope) نشت داد.
این الگو بهویژه در فریمورکهای رابط کاربری (UI) مانند React دردسرساز است، جایی که بهروزرسانیهای وضعیت (state updates) وابسته به