EduFlow
أساسيات JavaScript
⁦0%⁩

الوحدة 4 · JavaScript في المتصفّح

حفظ البيانات: JSON وlocalStorage

16 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

تحوّل البيانات إلى نص JSON وبالعكس باستخدام JSON.stringify وJSON.parse، وتعرف ما لا يستطيع JSON حمله، وتحفظ مصاريف Pocket وتحمّلها من localStorage بأمان.

  1. 1JSON: البيانات على شكل نص
  2. 2localStorage: مخزن صغير في المتصفّح

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

أضف ثلاثة مصاريف إلى Pocket، ثم أعِد تحميل الصفحة، وستجدها اختفت. كل ما بنيته يعيش في متغيّرات JavaScript، والمتغيّرات لا تعيش إلا ما دامت الصفحة. ولكي تتذكّر البيانات بين الزيارات، تحتاج إلى تخزينها في مكان ما، وأبسط مكان تستطيع صفحة ويب أن تخزّن فيه البيانات هو متصفّح المستخدم نفسه. لكن أولًا، يجب أن تتحوّل البيانات إلى نص.

JSON: البيانات على شكل نص

JSON (اختصار JavaScript Object Notation) صيغة نصية للبيانات. يكاد شكلها يطابق صيغة كتابة الكائنات والمصفوفات في JavaScript، وهذا ليس مصادفة، وهي اليوم الصيغة الأكثر شيوعًا لإرسال البيانات بين البرامج على الويب.

JavaScript
const expenses = [
  { id: "exp-1", label: "Coffee", amount: 450, tags: ["work"] },
  { id: "exp-2", label: "Train", amount: 1220, tags: [] },
];

const text = JSON.stringify(expenses);
console.log(typeof text);
console.log(text);

const back = JSON.parse(text);
console.log(back[1].label, back === expenses);

JSON.stringify تحوّل قيمة إلى نص JSON. وJSON.parse تقرأ نص JSON وتبني منه قيمًا جديدة. والنتيجة نسخة جديدة بالمحتويات نفسها، ولهذا تكون back === expenses تساوي false.

قواعد JSON أكثر صرامة من قواعد JavaScript: أسماء الخصائص يجب أن تكون بين علامات تنصيص مزدوجة، والنصوص لا تستخدم إلا العلامات المزدوجة، والفواصل الزائدة في النهاية غير مسموحة. نادرًا ما تكتب JSON يدويًا، لكن حين تفعل، تسبّب هذه القواعد الثلاث معظم أخطاء التحليل. وللحصول على نسخة مقروءة، تُزيح JSON.stringify(value, null, 2) المخرجات بمسافتين.

ما لا يستطيع JSON حمله

يعرف JSON النصوص والأرقام والقيم المنطقية وnull والمصفوفات والكائنات البسيطة. وكل ما عدا ذلك يتغيّر أو يضيع في الطريق:

JavaScript
const tricky = {
  when: new Date("2026-10-05T09:30:00Z"),
  note: undefined,
  format() { return "hi"; },
  tags: new Set(["work"]),
  ratio: NaN,
};
console.log(JSON.stringify(tricky));

التواريخ تصبح نصوصًا بصيغة ISO ولا تعود تواريخ، وخصائص undefined والدوال تُسقط، والـ Sets والـ Maps تصبح كائنات فارغة، وNaN تصبح null. ويتجنّب Pocket كل هذا بالتصميم: المبالغ أعداد صحيحة، والتواريخ تُخزَّن نصوصًا مثل "2026-10-05"، والفئات نصوص بسيطة. واختيار بيانات تناسب JSON منذ البداية يوفّر كثيرًا من كود التحويل لاحقًا.

localStorage: مخزن صغير في المتصفّح

يحصل كل موقع على مخزن صغير للمفاتيح والقيم في متصفّح كل مستخدم، اسمه localStorage. إنه ينجو من إعادة التحميل ومن إعادة تشغيل المتصفّح، ولا يحمل إلا النصوص:

storage.js
localStorage.setItem("pocket.theme", "dark");
console.log(localStorage.getItem("pocket.theme")); // "dark"
console.log(localStorage.getItem("nothing-here")); // null
localStorage.removeItem("pocket.theme");

لتخزين مصفوفة مصاريف، حوّلها إلى JSON عند الإدخال وحلّلها عند الإخراج:

الحفظ: تتحوّل مصفوفة المصاريف إلى نص JSON باستخدام JSON.stringify وتُخزَّن باستخدام setItem. التحميل: تُرجع getItem النص، وتعيده JSON.parse إلى مصفوفة جديدة. expenses مصفوفة كائنات نص JSON '[{"id":…}]' localStorage stringify setItem getItem parse
الحفظ والتحميل يمرّان عبر نص JSON
storage.js
const STORAGE_KEY = "pocket.expenses.v1";

function saveExpenses(expenses) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(expenses));
}

function loadExpenses() {
  try {
    const text = localStorage.getItem(STORAGE_KEY);
    const data = text === null ? [] : JSON.parse(text);
    return Array.isArray(data) ? data : [];
  } catch {
    return [];
  }
}

loadExpenses دفاعية عن قصد، لأنها تُنفَّذ مع كل تحميل للصفحة ويجب ألّا تُسقط التطبيق أبدًا. الزائر لأول مرة لم يحفظ شيئًا، فتُرجع getItem القيمة null ونبدأ بقائمة فارغة. والنص التالف يجعل JSON.parse ترمي خطأ، فتلجأ catch إلى قائمة فارغة. وبعض أوضاع الخصوصية في المتصفّحات تمنع التخزين كليًا، وهذا يرمي خطأ أيضًا يذهب إلى catch نفسها. وأخيرًا، تحمي Array.isArray من JSON صالح لكن بشكل خاطئ. والجزء .v1 في المفتاح يتيح لإصدار مستقبلي من Pocket أن يغيّر صيغة البيانات وينقل البيانات القديمة بدلًا من أن يسيء قراءتها.

ثم اربط كل ذلك: حمّل مرة واحدة عند البدء، واحفظ في كل مرة تتغيّر فيها البيانات، في المكان نفسه الذي تستدعي فيه renderExpenses أصلًا:

app.js
let expenses = loadExpenses();
renderExpenses();

function setExpenses(next) {
  expenses = next;
  saveExpenses(expenses);
  renderExpenses();
}

تمرير كل تغيير عبر دالة واحدة setExpenses يعني أنك لا تستطيع أن تنسى الحفظ في معالج حدث وتتذكّره في آخر.

لترى ما هو مخزّن، افتح DevTools، واذهب إلى لوحة Application في Chrome أو Edge (أو Storage في Firefox)، ووسّع Local Storage. يمكنك هناك قراءة المدخلات وتعديلها وحذفها، وهذه أسرع طريقة لاختبار ما يفعله تطبيقك مع البيانات المفقودة أو التالفة. وهناك أيضًا sessionStorage، بالدوال نفسها، وتُمسح حين يُغلق التبويب.

صار بإمكانك الاحتفاظ بالبيانات بين الزيارات. والمصدر الكبير الآخر للبيانات هو خادم على الإنترنت، والتحدّث إليه يعني انتظار الإجابة. والانتظار هو موضوع الدرس التالي.

أهم النقاط

  • JSON صيغة نصية للبيانات؛ JSON.stringify تحوّل القيم إلى نص JSON، وJSON.parse تعيد نص JSON إلى قيم.
  • يحمل JSON النصوص والأرقام والقيم المنطقية وnull والمصفوفات والكائنات البسيطة؛ أما الدوال وundefined فتُسقط، والتواريخ تصبح نصوصًا.
  • localStorage لا تخزّن إلا النصوص، لكل موقع على حدة، في متصفّح المستخدم؛ احفظ باستخدام setItem(key, JSON.stringify(data)).
  • يجب أن ينجو التحميل من غياب المفتاح (getItem تُرجع null) ومن النص التالف (JSON.parse ترمي خطأ)، لذلك ضعه داخل try/catch مع قيمة بديلة.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

استُدعيت localStorage.setItem("expenses", expenses) مع مصفوفة من الكائنات. ما الذي يُخزَّن؟

السؤال 2 من 3

ماذا يُنتج JSON.stringify({ label: "Tea", note: undefined, amount: 300 })؟

السؤال 3 من 3

يحمّل Pocket البيانات بـ JSON.parse(localStorage.getItem("pocket.expenses")) في الزيارة الأولى، ولم يُحفظ شيء بعد. ماذا يحدث؟

تدريب

بيئة تجريب الكود

احفظ Pocket وحمّله بأمان

دوال التخزين في Pocket تُضيّع البيانات. أصلحها مستخدمًا localStorage الحقيقية (وهي تعمل في بيئة التجربة هذه، لكنها تبدأ فارغة مع كل تشغيل):

  • أصلح saveExpenses(expenses) بحيث تخزّن المصاريف بصيغة JSON تحت STORAGE_KEY.
  • أصلح loadExpenses() بحيث تُرجع المصفوفة المحفوظة، وتُرجع مصفوفة فارغة [] بدلًا من الانهيار حين لا يكون هناك شيء محفوظ، أو حين لا يكون النص المحفوظ JSON صالحًا، أو حين يكون JSON صالحًا لكنه ليس مصفوفة.
// Pocket's storage helpers. In this playground, localStorage works but starts
// empty on every run; on a real page it keeps its data between visits.
const STORAGE_KEY = "pocket.expenses.v1";

function saveExpenses(expenses) {
  localStorage.setItem(STORAGE_KEY, expenses);
}

function loadExpenses() {
  return JSON.parse(localStorage.getItem(STORAGE_KEY));
}

saveExpenses([{ id: "exp-1", label: "Coffee", amount: 450 }]);
console.log(localStorage.getItem(STORAGE_KEY));
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • saveExpenses تخزّن المصاريف نصَّ JSON
  • المصاريف المحفوظة تُحمَّل كما هي دون تغيير
  • التحميل مع عدم وجود شيء محفوظ يُرجع []
  • تحميل JSON غير صالح يُرجع [] بدلًا من رمي خطأ
  • تحميل JSON ليس مصفوفة يُرجع []