אתה עוקב אחרי באג דרך שלושה קבצים רק כדי לגלות שפונקציית עזר תמימה שינתה את השם של משתמש. מעולם לא נגעת במקור. או כך לפחות חשבת. ב-JavaScript, השמה של משתנה אחד למשתנה אחר לא תמיד אומרת את מה שאתה מצפה לו. השפה מחלקת את הנתונים שלה לשתי אסטרטגיות אחסון, ושכחה באיזו מהן אתה משתמש היא הדרך שבה מוטציות שקטות מחלחלות לקוד בייצור (production).

כדי למנוע זאת, עליך לראות את ההבדל בין ערכים פרימיטיביים (primitive values) לבין אובייקטים בדיוק כפי שהמנוע עושה.

פרימיטיביים: עותקים אמיתיים

פרימיטיבי הוא נתון בודד ובלתי ניתן לחלוקה. לא ניתן לפרק אותו לערכי JavaScript קטנים יותר. השפה מגדירה שבעה סוגי פרימיטיביים: String, Number, Boolean, Undefined, Null, Symbol, ו-BigInt.

מכיוון שפרימיטיביים הם אטומיים, הם בדרך כלל חיים ישירות בתוך הקישור (binding) של המשתנה. כשאתה מעתיק משתנה פרימיטיבי אחד למשנה, המנוע משכפל את הנתונים עצמם. כל משתנה מקבל תא (slot) עצמאי משלו בזיכרון.

let a = "Alina";
let b = a;
b = "Ali";

console.log(a); // "Alina"
console.log(b); // "Ali"

כאן, a נשאר ללא שינוי. השמה מחדש של b יצרה ערך חדש לגמרי וכיוונה את b אליו, בעוד ש-a עדיין מחזיק במחרוזת המקורית שלו. התנהגות זו היא העתקה לפי ערך (copy by value). זה עובד באופן זהה עבור מספרים, בוליאנים, סימבולים ושאר משפחת הפרימיטיביים. ניתן להעביר אותם לפונקציות, להקצות אותם מחדש או להחזיר אותם מבלי לדאוג להשפעות לוואי (side effects) על משתנים שכנים.

אובייקטים: כתובות משותפות

אובייקטים הם שונים. אובייקט הוא מכולה מורכבת המקבצת יחד מספר חלקים של נתונים. קטגוריה זו כוללת אובייקטים רגילים (plain objects), מערכים (arrays), פונקציות, תאריכים וכל סוג אחר שאינו פרימיטיבי. מכיוון שמבנים אלו יכולים להיות גדולים ומקוננים, 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 נועלת את הקישור (binding) של המשתנה כך שלא ניתן להפנות אותו לכתובת חדשה. היא אינה נועלת את האובייקט עצמו.

const settings = { theme: "dark" };
settings.theme = "light";        // Works perfectly.
settings = { theme: "dark" };    // TypeError

מפתחים מצפים לעיתים קרובות ש-const יבטיח אי-שינוי (immutability). הוא לא עושה זאת. הוא רק מונע הקצאה מחדש של הרפרנס. אם ברצונך שהתוכן יהיה חתום (sealed), עליך לבצע העתקה מכוונת.

איפה הבאגים האמיתיים מסתתרים

באגים של רפרנסים כמעט ולא מופיעים כהקצאות מחדש ברורות של משתנים. הם מסתתרים בתוך קריאות לפונקציות.

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) בתוך הפונקציה נכתבה ישירות לתוך האובייקט של הקורא (caller). הפונקציה נראתה כמו טרנספורמציה פשוטה, אך היא דלפה מצב (state) מעבר לגבולות הטווח (scope).

התבנית הזו כואבת במיוחד בפריימוורקים של ממשק משתמש (UI) כמו React, שבהם עדכוני מצב תלויים