{"id":"l-js-4-4","courseId":"javascript-fundamentals","lang":"ar","kind":"lesson","summary":"تحوّل البيانات إلى نص JSON وبالعكس باستخدام JSON.stringify وJSON.parse، وتعرف ما لا يستطيع JSON حمله، وتحفظ مصاريف Pocket وتحمّلها من localStorage بأمان.","html":"<p>أضف ثلاثة مصاريف إلى Pocket، ثم أعِد تحميل الصفحة، وستجدها اختفت. كل ما بنيته يعيش في متغيّرات JavaScript، والمتغيّرات لا تعيش إلا ما دامت الصفحة. ولكي تتذكّر البيانات بين الزيارات، تحتاج إلى تخزينها في مكان ما، وأبسط مكان تستطيع صفحة ويب أن تخزّن فيه البيانات هو متصفّح المستخدم نفسه. لكن أولًا، يجب أن تتحوّل البيانات إلى نص.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">JSON: البيانات على شكل نص</h2>\n<p><strong>JSON</strong> (اختصار JavaScript Object Notation) صيغة نصية للبيانات. يكاد شكلها يطابق صيغة كتابة الكائنات والمصفوفات في JavaScript، وهذا ليس مصادفة، وهي اليوم الصيغة الأكثر شيوعًا لإرسال البيانات بين البرامج على الويب.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"0\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> expenses</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"exp-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, label: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Coffee\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amount: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">450</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, tags: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"work\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"exp-2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, label: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Train\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amount: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1220</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, tags: [] },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> text);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> back</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(back[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].label, back </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses);</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p><code dir=\"ltr\">JSON.stringify</code> تحوّل قيمة إلى نص JSON. و<code dir=\"ltr\">JSON.parse</code> تقرأ نص JSON وتبني منه قيمًا جديدة. والنتيجة نسخة جديدة بالمحتويات نفسها، ولهذا تكون <code dir=\"ltr\">back === expenses</code> تساوي <code dir=\"ltr\">false</code>.</p>\n<p>قواعد JSON أكثر صرامة من قواعد JavaScript: أسماء الخصائص يجب أن تكون بين علامات تنصيص مزدوجة، والنصوص لا تستخدم إلا العلامات المزدوجة، والفواصل الزائدة في النهاية غير مسموحة. نادرًا ما تكتب JSON يدويًا، لكن حين تفعل، تسبّب هذه القواعد الثلاث معظم أخطاء التحليل. وللحصول على نسخة مقروءة، تُزيح <code dir=\"ltr\">JSON.stringify(value, null, 2)</code> المخرجات بمسافتين.</p>\n<h3 id=\"sec-2\" tabindex=\"-1\">ما لا يستطيع JSON حمله</h3>\n<p>يعرف JSON النصوص والأرقام والقيم المنطقية و<code dir=\"ltr\">null</code> والمصفوفات والكائنات البسيطة. وكل ما عدا ذلك يتغيّر أو يضيع في الطريق:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"1\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tricky</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  when: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2026-10-05T09:30:00Z\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  note: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">undefined</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  format</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"hi\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  tags: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"work\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ratio: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">NaN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">};</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(tricky));</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>التواريخ تصبح نصوصًا بصيغة ISO ولا تعود تواريخ، وخصائص <code dir=\"ltr\">undefined</code> والدوال تُسقط، والـ Sets والـ Maps تصبح كائنات فارغة، و<code dir=\"ltr\">NaN</code> تصبح <code dir=\"ltr\">null</code>. ويتجنّب Pocket كل هذا بالتصميم: المبالغ أعداد صحيحة، والتواريخ تُخزَّن نصوصًا مثل <code dir=\"ltr\">&quot;2026-10-05&quot;</code>، والفئات نصوص بسيطة. واختيار بيانات تناسب JSON منذ البداية يوفّر كثيرًا من كود التحويل لاحقًا.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>التحليل دون شبكة أمان</span></p>\n<p>ترمي <code dir=\"ltr\">JSON.parse</code> خطأ <code dir=\"ltr\">SyntaxError</code> حين لا يكون النص JSON صالحًا، مثل قيمة كُتب نصفها، أو شيء خزّنه سكربت آخر تحت المفتاح نفسه. وإن حدث ذلك أثناء بدء تطبيقك، يفشل التطبيق كله في التحميل. وكل تحليل لنص لم تُنشئه أنت للتوّ مكانه داخل <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code>.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">localStorage: مخزن صغير في المتصفّح</h2>\n<p>يحصل كل موقع على مخزن صغير للمفاتيح والقيم في متصفّح كل مستخدم، اسمه <code dir=\"ltr\">localStorage</code>. إنه ينجو من إعادة التحميل ومن إعادة تشغيل المتصفّح، ولا يحمل <strong>إلا النصوص</strong>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">storage.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pocket.theme\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dark\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pocket.theme\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// \"dark\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"nothing-here\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// null</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">removeItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pocket.theme\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span></code></pre></div>\n<p>لتخزين مصفوفة مصاريف، حوّلها إلى JSON عند الإدخال وحلّلها عند الإخراج:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 200\" role=\"img\" aria-labelledby=\"l-js-4-4-t1\"><defs><marker id=\"arrow-l-js-4-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-js-4-4-t1\">الحفظ: تتحوّل مصفوفة المصاريف إلى نص JSON باستخدام JSON.stringify وتُخزَّن باستخدام setItem. التحميل: تُرجع getItem النص، وتعيده JSON.parse إلى مصفوفة جديدة.</title>\n  <rect class=\"d-box-primary\" x=\"10\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"85\" y=\"96\" text-anchor=\"middle\">expenses</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"116\" text-anchor=\"middle\">مصفوفة كائنات</text>\n  <rect class=\"d-box\" x=\"265\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"340\" y=\"96\" text-anchor=\"middle\">نص JSON</text>\n  <text class=\"d-code\" x=\"340\" y=\"116\" text-anchor=\"middle\">'[{\"id\":…}]'</text>\n  <rect class=\"d-box-accent\" x=\"520\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"595\" y=\"104\" text-anchor=\"middle\">localStorage</text>\n  <path class=\"d-arrow\" d=\"M160 82 L261 82\" marker-end=\"url(#arrow-l-js-4-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M415 82 L516 82\" marker-end=\"url(#arrow-l-js-4-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M516 114 L419 114\" marker-end=\"url(#arrow-l-js-4-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M261 114 L164 114\" marker-end=\"url(#arrow-l-js-4-4-1)\"/>\n  <text class=\"d-code\" x=\"212\" y=\"60\" text-anchor=\"middle\">stringify</text>\n  <text class=\"d-code\" x=\"467\" y=\"60\" text-anchor=\"middle\">setItem</text>\n  <text class=\"d-code\" x=\"467\" y=\"150\" text-anchor=\"middle\">getItem</text>\n  <text class=\"d-code\" x=\"212\" y=\"150\" text-anchor=\"middle\">parse</text>\n</svg><figcaption>الحفظ والتحميل يمرّان عبر نص JSON</figcaption></figure>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">storage.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> STORAGE_KEY</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"pocket.expenses.v1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> saveExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">expenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STORAGE_KEY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> loadExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STORAGE_KEY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> text </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Array.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isArray</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(data) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> data </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">loadExpenses</code> دفاعية عن قصد، لأنها تُنفَّذ مع كل تحميل للصفحة ويجب ألّا تُسقط التطبيق أبدًا. الزائر لأول مرة لم يحفظ شيئًا، فتُرجع <code dir=\"ltr\">getItem</code> القيمة <code dir=\"ltr\">null</code> ونبدأ بقائمة فارغة. والنص التالف يجعل <code dir=\"ltr\">JSON.parse</code> ترمي خطأ، فتلجأ <code dir=\"ltr\">catch</code> إلى قائمة فارغة. وبعض أوضاع الخصوصية في المتصفّحات تمنع التخزين كليًا، وهذا يرمي خطأ أيضًا يذهب إلى <code dir=\"ltr\">catch</code> نفسها. وأخيرًا، تحمي <code dir=\"ltr\">Array.isArray</code> من JSON صالح لكن بشكل خاطئ. والجزء <code dir=\"ltr\">.v1</code> في المفتاح يتيح لإصدار مستقبلي من Pocket أن يغيّر صيغة البيانات وينقل البيانات القديمة بدلًا من أن يسيء قراءتها.</p>\n<p>ثم اربط كل ذلك: حمّل مرة واحدة عند البدء، واحفظ في كل مرة تتغيّر فيها البيانات، في المكان نفسه الذي تستدعي فيه <code dir=\"ltr\">renderExpenses</code> أصلًا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> loadExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">next</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  expenses </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> next;</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  saveExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>تمرير كل تغيير عبر دالة واحدة <code dir=\"ltr\">setExpenses</code> يعني أنك لا تستطيع أن تنسى الحفظ في معالج حدث وتتذكّره في آخر.</p>\n<p>لترى ما هو مخزّن، افتح DevTools، واذهب إلى لوحة Application في Chrome أو Edge (أو Storage في Firefox)، ووسّع Local Storage. يمكنك هناك قراءة المدخلات وتعديلها وحذفها، وهذه أسرع طريقة لاختبار ما يفعله تطبيقك مع البيانات المفقودة أو التالفة. وهناك أيضًا <code dir=\"ltr\">sessionStorage</code>، بالدوال نفسها، وتُمسح حين يُغلق التبويب.</p>\n<aside class=\"callout callout-why\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>اعرف الحدود</span></p>\n<p>مساحة <code dir=\"ltr\">localStorage</code> نحو 5 ميغابايت لكل موقع، وهي متزامنة (فالحفظ الضخم يحجب الصفحة لحظة)، ويستطيع أي سكربت يعمل في موقعك قراءتها. إنها مناسبة للتفضيلات ولبيانات تطبيق شخصي. وغير مناسبة لكلمات المرور أو الـ tokens أو أي شيء حسّاس، ولا للبيانات التي يجب أن تتبع المستخدم إلى جهاز آخر، فتلك تحتاج إلى خادم.</p>\n</aside>\n<p>صار بإمكانك الاحتفاظ بالبيانات بين الزيارات. والمصدر الكبير الآخر للبيانات هو خادم على الإنترنت، والتحدّث إليه يعني انتظار الإجابة. والانتظار هو موضوع الدرس التالي.</p>\n","toc":[{"id":"sec-1","text":"JSON: البيانات على شكل نص","level":2},{"id":"sec-2","text":"ما لا يستطيع JSON حمله","level":3},{"id":"sec-3","text":"localStorage: مخزن صغير في المتصفّح","level":2}],"takeaways":["JSON صيغة نصية للبيانات؛ <code dir=\"ltr\">JSON.stringify</code> تحوّل القيم إلى نص JSON، و<code dir=\"ltr\">JSON.parse</code> تعيد نص JSON إلى قيم.","يحمل JSON النصوص والأرقام والقيم المنطقية وnull والمصفوفات والكائنات البسيطة؛ أما الدوال و<code dir=\"ltr\">undefined</code> فتُسقط، والتواريخ تصبح نصوصًا.","<code dir=\"ltr\">localStorage</code> لا تخزّن إلا النصوص، لكل موقع على حدة، في متصفّح المستخدم؛ احفظ باستخدام <code dir=\"ltr\">setItem(key, JSON.stringify(data))</code>.","يجب أن ينجو التحميل من غياب المفتاح (<code dir=\"ltr\">getItem</code> تُرجع <code dir=\"ltr\">null</code>) ومن النص التالف (<code dir=\"ltr\">JSON.parse</code> ترمي خطأ)، لذلك ضعه داخل <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code> مع قيمة بديلة."],"further":[{"title":"Working with JSON (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/JSON"},{"title":"JSON.stringify() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify"},{"title":"Window.localStorage (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"}],"quiz":[{"q":"<p>استُدعيت <code dir=\"ltr\">localStorage.setItem(&quot;expenses&quot;, expenses)</code> مع مصفوفة من الكائنات. ما الذي يُخزَّن؟</p>","options":[{"text":"النص <code dir=\"ltr\">&quot;[object Object],[object Object]&quot;</code>.","why":"صحيح. كل كائن يتحوّل إلى “[object Object]” فتضيع البيانات. خزّن <code dir=\"ltr\">JSON.stringify(expenses)</code> بدلًا من ذلك."},{"text":"المصفوفة، جاهزة للاستخدام حين تقرؤها مجددًا.","why":"<code dir=\"ltr\">localStorage</code> لا تخزّن إلا النصوص. وكل ما عدا ذلك يُحوَّل أولًا بـ <code dir=\"ltr\">String()</code>."},{"text":"لا شيء؛ <code dir=\"ltr\">setItem</code> ترمي خطأ مع القيم غير النصية.","why":"لا ترمي أي خطأ. إنها تحوّل القيمة إلى نص بصمت، ولهذا يسهل أن يفوتك هذا الخطأ."}],"answer":0},{"q":"<p>ماذا يُنتج <code dir=\"ltr\">JSON.stringify({ label: &quot;Tea&quot;, note: undefined, amount: 300 })</code>؟</p>","options":[{"text":"<code dir=\"ltr\">{&quot;label&quot;:&quot;Tea&quot;,&quot;amount&quot;:300}</code>","why":"صحيح. الخصائص التي قيمتها <code dir=\"ltr\">undefined</code> (والدوال) تُحذف، لأن JSON لا يملك طريقة لكتابتها."},{"text":"<code dir=\"ltr\">{&quot;label&quot;:&quot;Tea&quot;,&quot;note&quot;:undefined,&quot;amount&quot;:300}</code>","why":"<code dir=\"ltr\">undefined</code> ليست JSON صالحًا، لذلك لا تكتبها <code dir=\"ltr\">stringify</code> أبدًا داخل كائن."},{"text":"<code dir=\"ltr\">{&quot;label&quot;:&quot;Tea&quot;,&quot;note&quot;:null,&quot;amount&quot;:300}</code>","why":"<code dir=\"ltr\">stringify</code> تُسقط خصائص <code dir=\"ltr\">undefined</code> بدلًا من تحويلها إلى null. وحدها <code dir=\"ltr\">null</code> نفسها تُكتب null."}],"answer":0},{"q":"<p>يحمّل Pocket البيانات بـ <code dir=\"ltr\">JSON.parse(localStorage.getItem(&quot;pocket.expenses&quot;))</code> في الزيارة الأولى، ولم يُحفظ شيء بعد. ماذا يحدث؟</p>","options":[{"text":"يرمي خطأ، لأن <code dir=\"ltr\">getItem</code> ترمي خطأ مع المفاتيح غير المعروفة.","why":"<code dir=\"ltr\">getItem</code> تُرجع <code dir=\"ltr\">null</code> للمفتاح المفقود؛ ولا ترمي خطأ."},{"text":"يُرجع <code dir=\"ltr\">null</code>، لأن <code dir=\"ltr\">JSON.parse(null)</code> تعامل null على أنها نص JSON “null”.","why":"صحيح. ثم ينهار الكود لاحقًا حين يستدعي <code dir=\"ltr\">.map</code> على <code dir=\"ltr\">null</code>. استخدم قيمة بديلة بـ <code dir=\"ltr\">?? []</code> أو بفحص صريح."},{"text":"يُرجع مصفوفة فارغة.","why":"لا شيء هنا يُنشئ مصفوفة. عليك أن توفّر القيمة البديلة، المصفوفة الفارغة، بنفسك."}],"answer":1}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Pocket's storage helpers. In this playground, localStorage works but starts\n// empty on every run; on a real page it keeps its data between visits.\nconst STORAGE_KEY = \"pocket.expenses.v1\";\n\nfunction saveExpenses(expenses) {\n  localStorage.setItem(STORAGE_KEY, expenses);\n}\n\nfunction loadExpenses() {\n  return JSON.parse(localStorage.getItem(STORAGE_KEY));\n}\n\nsaveExpenses([{ id: \"exp-1\", label: \"Coffee\", amount: 450 }]);\nconsole.log(localStorage.getItem(STORAGE_KEY));\n"},"solution":{"js":"// Pocket's storage helpers. In this playground, localStorage works but starts\n// empty on every run; on a real page it keeps its data between visits.\nconst STORAGE_KEY = \"pocket.expenses.v1\";\n\nfunction saveExpenses(expenses) {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify(expenses));\n}\n\nfunction loadExpenses() {\n  try {\n    const text = localStorage.getItem(STORAGE_KEY);\n    const data = text === null ? [] : JSON.parse(text);\n    return Array.isArray(data) ? data : [];\n  } catch {\n    return [];\n  }\n}\n\nsaveExpenses([{ id: \"exp-1\", label: \"Coffee\", amount: 450 }]);\nconsole.log(localStorage.getItem(STORAGE_KEY));\n"},"checks":[{"id":"saves-json","test":"localStorage.clear();\nsaveExpenses([{ id: 'a', label: 'Tea', amount: 300 }]);\nconst raw = localStorage.getItem('pocket.expenses.v1');\ntry { const back = JSON.parse(raw); return Array.isArray(back) && back[0].label === 'Tea' && back[0].amount === 300; } catch { return false; }\n"},{"id":"round-trip","test":"localStorage.clear();\nconst data = [{ id: 'a', label: 'Tea', amount: 300 }, { id: 'b', label: 'Bus', amount: 270 }];\nsaveExpenses(data);\nconst back = loadExpenses();\nreturn Array.isArray(back) && back.length === 2 && back[1].label === 'Bus' && back[1].amount === 270 && back !== data;\n"},{"id":"missing","test":"localStorage.clear();\nconst out = loadExpenses();\nreturn Array.isArray(out) && out.length === 0;\n"},{"id":"corrupt","test":"localStorage.clear();\nlocalStorage.setItem('pocket.expenses.v1', '[{\"id\": \"a\", oops');\ntry { const out = loadExpenses(); return Array.isArray(out) && out.length === 0; } catch { return false; }\n"},{"id":"wrong-shape","test":"localStorage.clear();\nlocalStorage.setItem('pocket.expenses.v1', '{\"label\":\"not a list\"}');\ntry { const out = loadExpenses(); return Array.isArray(out) && out.length === 0; } catch { return false; }\n"}],"title":"احفظ Pocket وحمّله بأمان","prompt":"<p>دوال التخزين في Pocket تُضيّع البيانات. أصلحها مستخدمًا <code dir=\"ltr\">localStorage</code> الحقيقية (وهي تعمل في بيئة التجربة هذه، لكنها تبدأ فارغة مع كل تشغيل):</p>\n<ul>\n<li>أصلح <code dir=\"ltr\">saveExpenses(expenses)</code> بحيث تخزّن المصاريف بصيغة JSON تحت <code dir=\"ltr\">STORAGE_KEY</code>.</li>\n<li>أصلح <code dir=\"ltr\">loadExpenses()</code> بحيث تُرجع المصفوفة المحفوظة، وتُرجع مصفوفة فارغة <code dir=\"ltr\">[]</code> بدلًا من الانهيار حين لا يكون هناك شيء محفوظ، أو حين لا يكون النص المحفوظ JSON صالحًا، أو حين يكون JSON صالحًا لكنه ليس مصفوفة.</li>\n</ul>\n","hints":["اطبع ما يخزّنه كود البداية: <code dir=\"ltr\">[object Object]</code>. غلّف البيانات بـ <code dir=\"ltr\">JSON.stringify(...)</code> قبل <code dir=\"ltr\">setItem</code>.","<code dir=\"ltr\">getItem</code> تُرجع <code dir=\"ltr\">null</code> حين لا يكون هناك شيء محفوظ. افحص ذلك قبل التحليل.","ضع التحليل داخل <code dir=\"ltr\">try { … } catch { return []; }</code>، واختم بـ <code dir=\"ltr\">return Array.isArray(data) ? data : [];</code>."],"explanation":"<p>لا تحتفظ <code dir=\"ltr\">localStorage</code> إلا بالنصوص، لذلك يجب تحويل المصفوفة إلى نص بـ <code dir=\"ltr\">JSON.stringify</code> عند الإدخال؛ وإلا فإنها تُحوَّل بـ <code dir=\"ltr\">String()</code> وتصبح <code dir=\"ltr\">[object Object]</code>. ويحمي التحميل من ثلاثة أنواع مختلفة من الفشل: مفتاح مفقود (<code dir=\"ltr\">getItem</code> تُرجع <code dir=\"ltr\">null</code>)، ونص ليس JSON (<code dir=\"ltr\">JSON.parse</code> ترمي خطأ، فتحوّله <code dir=\"ltr\">catch</code> إلى قائمة فارغة)، وJSON بشكل خاطئ (<code dir=\"ltr\">Array.isArray</code> ترفضه). وكتلة <code dir=\"ltr\">try</code> نفسها تلتقط أيضًا حالة المتصفّحات التي تمنع التخزين كليًا، فلا يمكن أن ينهار تحميل الصفحة هنا.</p>\n","checkLabels":{"saves-json":"<code dir=\"ltr\">saveExpenses</code> تخزّن المصاريف نصَّ JSON","round-trip":"المصاريف المحفوظة تُحمَّل كما هي دون تغيير","missing":"التحميل مع عدم وجود شيء محفوظ يُرجع []","corrupt":"تحميل JSON غير صالح يُرجع [] بدلًا من رمي خطأ","wrong-shape":"تحميل JSON ليس مصفوفة يُرجع []"}},"words":617,"readMinutes":4}