قد تتبع خطأً برمجياً عبر ثلاثة ملفات لتكتشف في النهاية أن دالة مساعدة غير ضارة قد قامت بتغيير اسم مستخدم. لم تلمس الأصل أبداً. أو هكذا ظننت. في JavaScript، لا يعني تعيين متغير لآخر دائماً ما تتوقعه. تقسم اللغة بياناتها إلى استراتيجيتين للتخزين، ونسيان أي منهما تستخدم هو الطريقة التي تتسلل بها التغييرات الصامتة إلى كود الإنتاج.
لمنع حدوث ذلك، عليك أن ترى الفرق بين القيم الأولية (primitive values) والكائنات (objects) تماماً كما يراها المحرك.
القيم الأولية: نسخ فعلية
القيمة الأولية هي معلومة واحدة غير قابلة للتجزئة. لا يمكن تفكيكها إلى قيم JavaScript أصغر. تحدد اللغة سبعة أنواع أولية: String، وNumber، وBoolean، وUndefined، وNull، وSymbol، وBigInt.
ولأن القيم الأولية ذرية (atomic)، فهي تعيش عموماً مباشرة داخل ارتباط المتغير. عندما تنسخ متغيراً أولياً إلى آخر، يقوم المحرك بمضاعفة البيانات الفعلية. يحصل كل متغير على مكانه المستقل في الذاكرة.
let a = "Alina";
let b = a;
b = "Ali";
console.log(a); // "Alina"
console.log(b); // "Ali"
هنا، يظل a دون تغيير. أدت إعادة تعيين b إلى إنشاء قيمة جديدة تماماً وتوجيه b إليها، بينما لا يزال a يحتفظ بسلسلة النصوص الأصلية. هذا السلوك هو النسخ بالقيمة (copy by value). وهو يعمل بشكل متماثل مع الأرقام، والقيم المنطقية (booleans)، والرموز (symbols)، وبقية عائلة القيم الأولية. يمكنك تمريرها إلى الدوال، أو إعادة تعيينها، أو إرجاعها دون القلق بشأن الآثار الجانبية على المتغيرات الشقيقة.
الكائنات: عناوين مشتركة
الكائنات مختلفة. الكائن هو حاوية مركبة تجمع قطعاً متعددة من البيانات معاً. تشمل هذه الفئة الكائنات العادية (plain objects)، والمصفوفات (arrays)، والدوال (functions)، والتواريخ (dates)، وكل الأنواع الأخرى غير الأولية. ولأن هذه الهياكل يمكن أن تصبح كبيرة ومتداخلة، فإن 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). لكنها لا تفعل ذلك؛ فهي تمنع فقط إعادة تعيين المرجع. إذا كنت تريد حماية المحتويات من التغيير، فيجب عليك النسخ عن قصد.
أين تكمن الأخطاء الحقيقية
نادراً ما تظهر أخطاء المراجع كإعادة تعيين واضحة للمتغيرات. إنها تختبئ داخل استدعاءات الدوال.
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 نسخة من المرجع. أي تغيير في الخصائص داخل الدالة كُتب مباشرة في كائن المستدعي. بدت الدالة وكأنها عملية تحويل بسيطة، ومع ذلك فقد سربت الحالة (state) عبر حدود النطاق (scope boundaries).
هذا النمط مؤلم بشكل خاص في أطر عمل واجهة المستخدم مثل React، حيث تعتمد تحديثات الحالة على