۲. مرجع و تغییر داده؛ چرا ویرایش یک ردیف جای دیگری را عوض میکند؟
در برنامهٔ دفتر هزینه، کاربر یک ردیف را برای ویرایش باز میکند و سپس «انصراف» را میزند. انتظار طبیعی این است که فهرست اصلی تغییر نکند. اگر فرم و فهرست به همان شیء اشاره کنند، تایپ در فرم ممکن است پیش از ذخیره، دادهٔ اصلی را تغییر دهد. برای برنامهنویس سطح متوسط، شناخت این رفتار از حفظکردن نام روشهای کپی مهمتر است.
در این درس، کدها را مستقل در Node.js ۲۴ یا کنسول مرورگر اجرا کنید. دادهها کوچکاند تا تغییرها قابل مشاهده باشد. در پروژهٔ واقعی، ابتدا مشخص کنید کدام بخش مالک داده است و تغییر در چه نقطهای قطعی میشود؛ سپس روش کپی را انتخاب کنید.
کپی آرایه، کپی ردیف نیست
const original = [{ id: 'e1', amount: 100000, meta: { note: 'اولیه' } }];
const next = [...original];
next[0].amount = 120000;
console.log(original[0].amount); // 120000
console.log(next === original); // false
console.log(next[0] === original[0]); // trueآرایهٔ بیرونی تازه ساخته شده، اما هر دو خانه هنوز به یک شیء اشاره میکنند. نوشتن const هم شیء را ثابت نمیکند؛ فقط اجازه نمیدهد همان نام را به مقدار دیگری نسبت دهید. اگر همین آزمایش را با Object.freeze(original) انجام دهید، فقط خود آرایه منجمد میشود و شیءهای درون آن لزوماً منجمد نیستند. انجماد سطحی را با حفاظت کامل اشتباه نگیرید.
اکنون همان عملیات را با جایگزینی ردیف انجام میدهیم. بهجای تغییر مستقیم، فقط ردیف هدف را از نو میسازیم:
const expenses = [{ id: 'e1', amount: 100000 }, { id: 'e2', amount: 80000 }];
const updated = expenses.map(item =>
item.id === 'e1' ? { ...item, amount: 120000 } : item
);
console.log(expenses[0].amount, updated[0].amount); // 100000 120000
console.log(updated[0] === expenses[0]); // false
console.log(updated[1] === expenses[1]); // trueاشتراک ردیف دوم در اینجا عمدی است؛ چون آن را تغییر نمیدهیم. مزیت این روش روشنبودن محل تغییر است. لازم نیست برای هر تغییر کوچک، کل دادهٔ برنامه را عمیق کپی کنیم. در فایل projects/expenses/model.mjs نیز تابع upsert با ساختن آرایهٔ نتیجه کار میکند و اعتبارسنجی را پیش از پذیرش ردیف انجام میدهد.
دادهٔ تو در تو را آگاهانه کپی کنید
گسترش شیء هم سطحی است. اگر ردیف دارای meta باشد، برای تغییر meta.note باید همان سطح را نیز جایگزین کنید:
const row = { id: 'e1', meta: { note: 'اولیه', source: 'manual' } };
const draft = { ...row, meta: { ...row.meta, note: 'نسخهٔ فرم' } };
console.log(row.meta.note); // اولیه
console.log(draft.meta.note); // نسخهٔ فرماین الگو برای ساختارهای کوچک و مشخص مناسب است، چون از روی کد میتوان مسیر تغییر را دید. اگر ساختار بسیار عمیق شده، پیش از افزودن چندین گسترش تو در تو، طراحی داده را بازبینی کنید. مثلاً نگهداری شناسهٔ محصول در سبد بهجای یک کپی بزرگ از محصول، هم ابهام قیمت را کم میکند و هم ویرایش را سادهتر میسازد.
structuredClone برای پیشنویس مستقل
وقتی واقعاً به نسخهای مستقل از دادهٔ سازگار نیاز دارید، structuredClone انتخاب قابل بررسی است. این مثال مستقل را اجرا کنید:
const source = {
title: 'گزارش',
settings: { currency: 'TOMAN' },
generatedAt: new Date('2026-01-01T00:00:00Z')
};
const copy = structuredClone(source);
copy.settings.currency = 'IRR';
console.log(source.settings.currency); // TOMAN
console.log(copy.generatedAt instanceof Date); // trueاین ابزار راه عمومی کپی هر چیز در برنامه نیست. تابع و عنصر DOM از جمله دادههایی نیستند که بتوان به این شکل کپی کرد. کپی یک نمونهٔ کلاس نیز جای قرارداد صریح ساخت مدل را نمیگیرد. وقتی داده از فایل یا شبکه میآید، مستقلبودن کپی، معتبربودن آن را تضمین نمیکند؛ هنوز باید فیلدها و محدودیتها بررسی شوند. کپی عمیق و اعتبارسنجی دو هدف جدا دارند.
تبدیل رفتوبرگشتی با JSON.stringify و JSON.parse را هم کپی بینقص همهٔ انواع ندانید. در مثال بالا، تاریخ پس از JSON به متن تبدیل میشود. اگر قرارداد پشتیبان شما فقط متن، عدد، آرایه و شیء ساده دارد، JSON برای انتقال مناسب است؛ انتخاب آن باید بر اساس قرارداد باشد، نه عادت.
یک راه ساده برای یافتن تغییر ناخواسته این است که پیش از اجرای عملیات، خروجی مورد انتظار و مرجعهای مشترک را روی کاغذ مشخص کنید. اگر ردیفها شامل دادهٔ حساس باشند، نسخهٔ پیشنویس را بیدلیل در کنسول چاپ نکنید. در تمرین فعلی از دادهٔ ساختگی استفاده میکنیم و میتوانیم قبل و بعد عملیات، مقدارها را با هم مقایسه کنیم. این بازبینی بهویژه هنگام افزودن دکمهٔ «انصراف» مفید است، چون ذخیرهٔ موفق بهتنهایی این خطا را نشان نمیدهد.
تمرین و پاسخ تشریحی
یک تابع editNote(rows, id, note) بنویسید که آرایهٔ تازه برگرداند، یادداشت ردیف هدف را تغییر دهد و آرایه و meta قبلی را دستنخورده نگه دارد. اگر شناسه پیدا نشد، سیاست روشنی داشته باشید؛ در این تمرین همان آرایهٔ ورودی را برگردانید و این رفتار را در آزمون ثبت کنید.
پاسخ راهنما: با some وجود شناسه را بررسی کنید، سپس map بزنید و فقط برای ردیف هدف، هم شیء ردیف و هم meta را گسترش دهید. یادداشت باید متن باشد و محدودیت طول داشته باشد. یک آزمون مقدار، یک آزمون هویت مرجع و یک آزمون انصراف از فرم بنویسید. معیار تحویل فقط نمایش متن تازه نیست؛ پیشنویس باید بدون اثر جانبی کنار گذاشته شود. در بازبینی کد توضیح دهید چرا کپی کامل همهٔ ردیفها برای همین مسئله لازم نبود.
منابع رسمی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
تألیف و مثالهای اختصاصی: تحریریه درسآوند.