إن كان HTML هو هيكل الصفحة وCSS هو مظهرها، فإنّ JavaScript هو ما يمنحها الحياة: التفاعل، تحديث المحتوى دون إعادة تحميل، والتواصل مع الخوادم. تطوّرت اللغة كثيراً في السنوات الأخيرة وصارت أنظف وأقوى. هذا الدليل يضع بين يديك الأساسيات الحديثة التي تبني عليها أوّل مشاريعك بثقة.
1. المتغيّرات: let و const لا var
انسَ var القديم. اليوم نستعمل اثنين فقط:
constلقيمة لا يُعاد تعيينها (الافتراضي — استعمله أوّلاً دائماً).letلقيمة ستتغيّر (عدّاد في حلقة مثلاً).
كلاهما «محصور بالكتلة» (block-scoped)، أي يعيش داخل الأقواس { } التي عُرّف فيها فقط — وهذا يمنع كثيراً من الأخطاء الغامضة التي كان يسبّبها var.
2. الدوال والدوال السهمية
الدالّة تُغلّف منطقاً قابلاً لإعادة الاستخدام. الصياغة الحديثة المختصرة هي «الدالّة السهمية»:
const square = (n) => n * n;
square(5); // 25
أبقِ كل دالّة مسؤولة عن مهمّة واحدة واضحة، وأعطِها اسماً يصف ما تفعله (calculateTotal لا doStuff). الدوال الصغيرة المسمّاة جيداً هي عمود الكود القابل للقراءة.
3. المصفوفات والكائنات
هما وعاءا البيانات الأساسيان. المصفوفة [] لقائمة مرتّبة، والكائن {} لبيانات ذات مفاتيح:
const user = { name: 'سارة', age: 28 };
const skills = ['HTML', 'CSS', 'JS'];
تعلّم توابع المصفوفات الحديثة فهي تغنيك عن حلقات كثيرة: map للتحويل، filter للتصفية، find للبحث، reduce للتجميع. مثال: skills.map(s => s.toLowerCase());
4. التعامل مع الصفحة (DOM)
الـDOM هو تمثيل الصفحة الذي يستطيع جافاسكربت قراءته وتعديله:
const btn = document.querySelector('#send');
btn.textContent = 'إرسال';
تنتقي العناصر بـquerySelector (بمحدِّد CSS نفسه)، وتغيّر نصّها أو أصنافها أو سماتها. نصيحة أمان مهمّة: لإدراج نصّ من المستخدم استعمل textContent لا innerHTML لتتجنّب ثغرات حقن السكربت (XSS).
5. الأحداث (Events)
التفاعل يبدأ بالاستماع لحدث ثم الردّ عليه:
btn.addEventListener('click', () => {
console.log('تم الضغط!');
});
أشهر الأحداث: click، submit للنماذج، input لحقول الكتابة. في معالِج النموذج استدعِ event.preventDefault() لمنع إعادة تحميل الصفحة الافتراضية ثم عالِج البيانات بنفسك.
console.log() بسخاء لتفهم ماذا يحدث وأين، فالتعلّم بالتجربة المباشرة أسرع طريق للإتقان.
6. غير المتزامن: Promises و async/await
جلب البيانات من خادم يستغرق وقتاً، وأثناءه يجب ألّا يتجمّد الموقع. هنا يأتي الكود «غير المتزامن». الصياغة الحديثة async/await تجعله يُقرأ كأنه متزامن:
async function getData() {
try {
const res = await fetch('/api/data');
const data = await res.json();
return data;
} catch (e) { console.error(e); }
}
غلّف دائماً طلبات الشبكة بـtry/catch لأنها قد تفشل (انقطاع، خطأ خادم)، وتعامل مع الفشل برسالة واضحة للمستخدم.
7. أخطاء المبتدئين الشائعة
- الخلط بين
=(تعيين) و===(مقارنة). استعمل===دائماً للمقارنة الصارمة. - نسيان أنّ
fetchغير متزامن، فتقرأ النتيجة قبل وصولها. - تشغيل السكربت قبل تحميل العناصر — ضع
<script>بـdeferأو في نهاية<body>. - تلويث النطاق العام بمتغيّرات كثيرة — غلّف كودك في دوال أو وحدات (modules).
إلى هنا انتهت الأساسيات النظرية. الأقسام الخمس التالية مختلفة: كل نمط فيها مأخوذ من ملف جافاسكربت واحد بحجم يقارب سبعة آلاف سطر، يشغّل موقعاً عربياً حقيقياً (منتدى + متابعة حمل) يعمل في المتصفّح وداخل غلاف تطبيق أندرويد في الوقت نفسه. سترى فيها لماذا تصير التفاصيل الصغيرة التي تبدو ترفاً في الدروس ضرورةً في الإنتاج.
8. السلسلة الاختيارية ?. والدمج الصفري ?? — الموضع الذي يجعلهما ضرورة
أوضح مثال حقيقي هو دالّة تكشف هل نحن داخل غلاف تطبيق أصلي أم في متصفّح عادي:
function isNativeApp() {
try { return !!(window.Capacitor?.isNativePlatform?.() || window.WasanNative?.isNative); }
catch (_) { return false; }
}
اقرأ السطر ببطء، فكل رمز فيه لسبب:
window.Capacitorغير موجود على الويب إطلاقاً — يحقنه غلاف التطبيق فقط. لو كُتبتwindow.Capacitor.isNativePlatform()بلا?.لرمى المتصفّحTypeErrorعلى كل زيارة من الويب، ولتوقّف تنفيذ ما بعده في نفس المسار. الفرق هنا ليس «أناقة» بل صفحة تعمل مقابل ميزة تختفي.- لاحظ أنّ
?.مكرّرة مرّتين: واحدة على الكائن وأخرى على الدالّة (isNativePlatform?.()). السبب أنّ وجود الكائن لا يضمن وجود الدالّة — نسخة أقدم من الغلاف قد تحمل الكائن بلا هذه الدالّة، فتحصل على خطأ «ليست دالّة» بدل شرط يعطيfalseبهدوء. - المصدر الثاني
window.WasanNative?.isNativeعلم بديل يضعه جسر التطبيق بعد تهيئته، فيبقى الكشف صحيحاً لو تغيّر شكل الواجهة الأولى. !!تحوّل الناتج إلى قيمة منطقية صريحة، وtry/catchطبقة أخيرة لأن قراءة خاصية منwindowقد ترمي في بيئات مقيّدة.
هذه الدالّة تُستدعى قبل حقن كود طرف ثالث في الصفحة، ويُمنع الحقن داخل التطبيق لأسباب تتعلّق بسياسات المتاجر. تخيّل لو رمى الشرط خطأً: لا رسالة للمستخدم، فقط ميزة لا تعمل. لمن يريد فهم بيئة الغلاف الأصلي أكثر، راجع من موقع إلى تطبيق أندرويد بـ Capacitor.
أمّا الدمج الصفري ?? فأصدق مثال عليه دالّة تبني مفتاح «اليوم» بتوقيت الرياض:
function riyadhDateKey(ms) {
return new Date((ms ?? Date.now()) + 3 * 60 * 60 * 1000)
.toISOString().slice(0, 10);
}
الوسيط طابع زمني بالميلي ثانية، وقد يُستدعى بلا وسيط. ms ?? Date.now() تعني: استخدم القيمة إلّا إن كانت null أو undefined. لو كُتبت ms || Date.now() لكان الصفر — وهو طابع زمني صحيح تماماً — يسقط إلى الوقت الحالي، فينتج مفتاح يوم خاطئ في عدّاد يومي بلا أي رسالة خطأ.
والعكس صحيح ومقصود في الملف نفسه: Math.max(3, Math.min(50, Number(frequency) || 6)). هنا || هي الصواب، لأن التردّد صفر ليس قيمة مشروعة أصلاً فيجب أن يسقط إلى الافتراضي 6، والحصر بين 3 و50 موجود لأن الرقم يأتي من لوحة إدارة يكتبها بشر قد يُخطئ.
false جواب مشروع في هذا الموضع؟» — إن كان نعم فاستعمل ??، وإن كانت كل القيم الفارغة خطأً فاستعمل ||. معظم الأخطاء الصامتة في هذا الباب سببها استعمال || مع قيمة رقمية يصحّ أن تكون صفراً.
9. تهريب النصّ: عند الإدراج لا عند التخزين
القاعدة التي ذكرناها في قسم DOM (textContent لا innerHTML) تكفي للنصوص المفردة. لكن أي تطبيق حقيقي يبني قوائم كاملة كسلاسل HTML لأسباب أداء، وحينها تحتاج دالّة تهريب. الدالّة المستعملة فعلياً بسيطة إلى حدّ مفاجئ:
function escapeHtml(s) {
if (s == null) return '';
return String(s).replace(/[&<>"']/g, c => MAP[c]);
}
حيث MAP يحوّل خمسة أحرف فقط: & إلى &، و< إلى <، و> إلى >، و" إلى "، و' إلى '. وفي هذه الأسطر القليلة أربعة قرارات:
String(s)قبل الاستبدال: القيمة قد تصل رقماً أو كائناً من قاعدة البيانات، وreplaceليست موجودة إلّا على النصوص.if (s == null)بمساواة فضفاضة مقصودة: تلتقطnullوundefinedفي اختبار واحد. هذا هو الاستثناء الوحيد المقبول عملياً لقاعدة «استعمل===دائماً» التي ذكرناها في قسم الأخطاء.- تعبير نمطي واحد بمرور واحد على النصّ، لا خمس عمليات
replaceمتتابعة. لو فُصلت لوجب أن تبدأ بـ&حتماً، وإلّا صار<أوّلاً<ثم تحوّلت علامة&فيه إلى كيان فينتج نصّ مشوّه. المرور الواحد يُلغي المشكلة من أساسها. - لماذا تُهرَّب الفاصلة العليا
'وهي غير خطرة في نصّ HTML عادي؟ لأن نفس القيمة تُدرَج في موضع آخر داخل سمة حدث:onclick="toggleFavName('${escapeHtml(item.name)}')". اسمٌ يحتوي فاصلة عليا يكسر سلسلة جافاسكربت هناك ويفتح باب تنفيذ كود. النصّ يعبر ثلاثة سياقات — نصّ HTML، وسمة، وسلسلة JS — فيجب أن يغطّي التهريب أضيقها.
أمّا سؤال «لماذا عند الإدراج لا عند التخزين؟» فجوابه ثلاثة أسباب عملية. الأول أنّ النصّ الواحد يُعرض في أماكن متعدّدة: بطاقة موضوع، ردّ، تقرير مُهيَّأ للطباعة، لوحة إدارة — وبعضها ليس HTML أصلاً، ولكل سياق تهريبه المناسب. الثاني أنّ التخزين مهرَّباً يعني أنّ الكاتبة ترى " بدل علامة التنصيص عند تعديل موضوعها، ثم يتضاعف التهريب مع كل حفظ. الثالث أنّ قواعد قاعدة البيانات تتحقّق من النوع والطول — في هذا المشروع مثلاً عنوان الموضوع 40 حرفاً كحدّ أعلى، والمتن 300، والردّ 200 — وهي لا تفحص محتوى النصّ ولا يمكنها ذلك؛ فالحماية من الحقن مسؤولية طبقة العرض وحدها.
القاعدة النهائية بسيطة: إن كان النصّ سيصبح نصّاً فاستعمل textContent ولا تهرّب شيئاً بيدك، وإن كنت تبني سلسلة HTML بقالب نصّي فكل قيمة متغيّرة داخله تُغلَّف بدالّة التهريب بلا استثناء واحد.
10. الخطأ الذي كلّفنا ساعات: innerHTML لا يُنفّذ السكربت
الحالة: كود من طرف ثالث (وسم <script> جاهز) يُحفظ في إعدادات لوحة الإدارة ثم يُحقن في الصفحة عند الحاجة. الكود صحيح، والحقن يبدو ناجحاً تماماً، والوسم موجود فعلاً في شجرة الصفحة إن فتحت المتفقّد… ولا شيء يعمل. ولا خطأ في الطرفية.
السبب من مواصفة الويب نفسها: أي <script> يصل إلى المستند عبر innerHTML يُعلَّم «غير قابل للتنفيذ» ولا يشغّله المتصفّح أبداً. لا استثناء ولا تحذير — فشل صامت، وهو أسوأ أنواع الفشل لأنه لا يترك أثراً تبحث عنه. الحل هو إعادة إنشاء الوسم بعد الحقن:
holder.querySelectorAll('script').forEach(oldS => {
const s = document.createElement('script');
for (const a of oldS.attributes) s.setAttribute(a.name, a.value);
if (oldS.textContent) s.textContent = oldS.textContent;
oldS.parentNode.replaceChild(s, oldS);
});
ولكل سطر سبب تعلّمناه بالتجربة:
- العنصر الذي أنشأه
createElementثم أُدخل في المستند قابل للتنفيذ — الفرق كله في طريقة الإنشاء لا في المحتوى. - نسخ كل السمات في حلقة، لا
srcوحده. مزوّدو الأكواد الخارجية يقرؤون سماتهم الخاصة (async،type، وسماتdata-*)، وسكربت فقد سمته الخاصة يحمّل نفسه ثم لا يفعل شيئاً — وهذا فشل صامت ثانٍ فوق الأول. replaceChildلاappendChild: الموضع في الشجرة يهمّ بعض الأكواد، وترك الوسم القديم يُبقي نسخة ميتة تشوّش أي تشخيص لاحق.- الحاوية نفسها تُنشأ بـ
holder.style.display = 'contents'كي لا يضيف عنصر الحقن أي أثر على التخطيط.
ويلزم النمطَ حارسان: علم منطقي يمنع الحقن مرّتين في نفس تحميل الصفحة، وWeakSet من العناصر التي فُعِّلت سكربتاتها لمنع تكرار التفعيل عند إعادة رسم القائمة. اختيار WeakSet تحديداً لا Set مقصود: العناصر تُحذف من الشجرة كثيراً عند إعادة الرسم، والمرجع الضعيف يسمح لجامع القمامة بأخذها بدل أن تتراكم في مجموعة تحفظها للأبد.
وهناك سبب ثالث للفشل يستحقّ الذكر لأنه يشبه هذا تماماً من الخارج: لو منعت سياسة أمان المحتوى نطاق السكربت الخارجي، فالوسم صحيح والتنفيذ ممنوع، والرسالة تظهر في الطرفية فقط لا في الواجهة. تفصيل ذلك في سياسة أمان المحتوى مع أطراف ثالثة.
innerHTML وفيها <script> هي سكربت لن يعمل. إمّا تعيد إنشاء الوسم كما في المثال أعلاه، وإمّا تضع الكود من البداية داخل إطار معزول (iframe) تُكتب فيه الصفحة بالكامل فيعمل السكربت طبيعياً.
11. غير المتزامن في الإنتاج: لكل طلب مسار فشل
قسم async/await أعلاه يعلّمك الصياغة. أمّا الفرق بين كود يعمل على جهازك وكود يعمل لآلاف المستخدمين فهو أن كل عملية شبكة تُكتب ولها مسار فشل معروف مسبقاً، لا تفاؤل. خمسة أنماط من الملف نفسه:
أولاً: محلي فوراً، ثم السحابة بتأخير. دالّة الحفظ تكتب في localStorage فوراً بلا انتظار، ثم تؤجّل الكتابة السحابية 1500 مللي ثانية بعد آخر تغيير عبر clearTimeout ثم setTimeout. النتيجة كتابة واحدة لكل جلسة تعديل بدل كتابة لكل ضغطة زر — أثر مباشر على سرعة الإحساس بالتطبيق وعلى فاتورة قاعدة البيانات معاً، وهو ما يُفصّله مقال ضبط تكلفة Firestore.
ثانياً: الفشل يُدار ولا يُخفى. في دالّة الحفظ السحابي لا يكتفي catch بطباعة الخطأ: يرفع علماً باسم pendingSync ويُظهر للمستخدمة نصّاً صريحاً «فشلت المزامنة – ستُعاد لاحقاً». والبيانات لم تُفقد أصلاً لأنها كُتبت محلياً قبل المحاولة. هذا هو الفرق العملي بين catch يبتلع المشكلة وcatch يُدير عاقبتها.
ثالثاً: استجابة ناجحة لا تعني بيانات صالحة. ثلاث حمايات في ثلاثة أسطر من فحص إصدار خفيف:
const r = await fetch('./version.json?t=' + Date.now(), { cache: 'no-store' });
if (!r.ok) return null;
const j = await r.json().catch(() => null);
الأولى ?t= لكسر كاش متصفّحات الجوال. والثانية !r.ok لأن fetch — خلافاً لما يتوقّعه معظم المبتدئين — لا يرمي خطأً عند 404 أو 500، بل يرمي عند فشل الشبكة فقط، فالحالة 404 تصل إليك «نجاحاً» ثم تنفجر بعد سطرين. والثالثة .catch على json() لأن الخادم قد يردّ 200 بصفحة HTML لخطأ ما، فيرمي المحلّل. ولهذا الفحص علاقة مباشرة بإدارة إصدارات الكاش، وتفصيلها في إدارة إصدارات Service Worker.
رابعاً: finally ليست ترفاً. نفس الدالّة ترفع علماً _versionFetchPending في أوّلها لتمنع طلبين متزامنين، وتصفّره في finally. بلا finally، أول فشل شبكة يترك العلم مرفوعاً فلا يعمل الفحص الدوري بعدها أبداً — عيب لا يظهر في التطوير ولا يُكتشف إلّا بعد أول انقطاع اتصال حقيقي عند مستخدم.
خامساً: المهلة الزمنية جزء من العقد. انتظار ردّ من Service Worker غُلّف في وعد فيه سباق بين الردّ ومهلة 1200 مللي ثانية، مع علم done يضمن ألّا يُحلّ الوعد مرّتين لو وصل الردّ واكتملت المهلة معاً. أي انتظار لطرف قد لا يجيب يحتاج مهلة، وإلّا انتظرت للأبد بصمت. ومثلها المستمع الحيّ للبيانات: يتجاهل اللقطات القادمة من الكاش المحلي بشرط snap.metadata.fromCache === false، ويقارن البيانات الواصلة بالحالة الحاضرة قبل الكتابة، وإلّا صار صدى كتابتك سبباً في إعادة رسم لا تنتهي.
12. دورة حياة الكود: حالة الوحدة، إلغاء الاشتراك، وحارس المرّة الواحدة
أ) حالة الوحدة مقابل النطاق العام. متغيّرات المستوى الأعلى في الملف — مثل let adsConfig = null; وlet unsubAds = null; — ليست خصائص للنافذة: window.adsConfig تعيد undefined مهما كانت قيمتها الحقيقية. وهذا مقصود لا نقص: نطاق عام أنظف وتصادم أسماء أقلّ. وما يجب أن يُنادى من خارج الملف يُنشر صراحةً وحده، مثل دالّة إعادة الرسم التي تناديها طبقة تغيير اللغة، أو الكائن window.WasanNative = { isNative: true } الذي يضعه جسر التطبيق ليقرأه ملف آخر.
والأثر العملي يظهر عند التشخيص. خطوة البناء تصغّر الملفات بـesbuild مع الخيار drop: ['console', 'debugger']، أي أن كل console.log كتبته للتشخيص يُحذف من النسخة المنشورة. لذلك التشخيص في الإنتاج لا يكون بالطرفية بل بثلاث طرق: أثر مرئي مقصود للمستخدم (مؤشّر حالة أو تنبيه)، وحالة تُنشر على window عن قصد لتُقرأ، وسجل الشبكة في أدوات المطوّر — فالطلبات لا تُحذف مع رسائل التشخيص.
وفي نفس خطوة البناء قرار يستحقّ الانتباه: bundle: false، أي تصغير بلا حزم. السبب أنّ الحزم يجعل المصغّر يعيد تسمية أسماء المستوى الأعلى، وصفحات المشروع تحتوي معالجات مضمّنة مثل onclick="toggleFavName('…')" تنادي دوالاً بأسمائها كما هي، فأي إعادة تسمية تكسرها كلها بصمت. وإلى جانبه خيار charset: 'utf8' لسبب مختلف: بدونه يحوّل المصغّر كل حرف عربي إلى تمثيل \uXXXX فيتضخّم الملف عشرات الكيلوبايتات — تفصيل ذلك في تصغير كود عربي بـ esbuild.
ب) من يُغلق ما فتحته؟ كل مستمع حيّ لقاعدة البيانات يُحتفظ بدالّة إلغائه في متغيّر خاص به (مستمع المواضيع، والتنبيهات، والرسائل، والردود، ومزامنة بيانات المستخدمة، وإعدادات الإعلانات)، ودالّة واحدة تُطفئها جميعاً عند تسجيل الخروج فتنادي كل دالّة إلغاء وتصفّر متغيّرها إلى null. وأربع ملاحظات تعلّمناها في هذا الباب:
- الترتيب مهم عند بدء مستمع جديد: دالّة بدء المزامنة تفتح بـ
if (unsubscribeSync) unsubscribeSync();قبل إنشاء المستمع. بدون هذا السطر، خروجٌ ودخولٌ بحساب آخر يترك مستمعين لوثيقتين مختلفتين يتناوبان الكتابة على الحالة نفسها — وهو عيب يظهر كبيانات «ترجع للخلف» بلا سبب مفهوم. - الإلغاء وحده لا يكفي، يجب تصفير المتغيّر. مؤقّت عدّاد يُبدأ بالحارس
if (!kickTimerInterval)، ودالّة التصفير تفعل الأمرين معاً:clearInterval(kickTimerInterval); kickTimerInterval = null;. لو أُلغي المؤقّت وبقي المتغيّر يحمل رقمه القديم، لظنّ الحارس أنّ مؤقّتاً يعمل ولما بدأ واحداً جديداً أبداً. - كل مؤقّت متكرّر يحتاج شرط خروج. فحص وصول محتوى داخل إطار يعمل كل 500 مللي ثانية بعدّاد محاولات، وعند 30 محاولة (نحو 15 ثانية) يوقف نفسه بـ
clearInterval. مؤقّت بلا شرط خروج يبقى يعمل ما بقي التبويب مفتوحاً، وقد يبقى ساعات. - المؤقّت الواحد يُستبدل لا يُكرّر: دالّة الحفظ ومؤشّر الحالة كلاهما ينادي
clearTimeoutعلى المؤقّت السابق قبل جدولة جديد، وإلّا تراكمت جدولات يلغي بعضها أثر بعض بترتيب لا تتحكّم فيه.
ج) حارس المرّة الواحدة. النمط أبسط ما يمكن: علم منطقي أعلى الملف وسطر واحد في أوّل الدالّة.
let _globalAdInjected = false;
function applyGlobalAdScript() {
if (_globalAdInjected) return; // مرّة واحدة لكل تحميل صفحة
// … شروط أخرى: هل الميزة مفعّلة؟ هل نحن على الويب؟ …
_globalAdInjected = true;
}
يتكرّر هذا النمط في الملف بأعلام صريحة: تهيئة الإشعارات، وربط مستمع الرسائل الواردة، وتشغيل مستمعي المنتدى، وإعادة تحميل الصفحة بعد تحديث. وكلّها تُنادى من أكثر من مسار — تسجيل دخول، تبديل تبويب، عودة التبويب إلى الواجهة، رسالة من Service Worker — وبلا الحارس تحصل على عيوب حقيقية: طلب إذن إشعارات مرّتين، أو تنبيه مزدوج لكل رسالة واردة، أو حلقة إعادة تحميل لا تنتهي.
وفي هذا النمط تفصيلة دقيقة: أين تضع = true؟ إن كان الهدف «لا تُنفَّذ هذه التهيئة مرّتين مهما حدث» فارفع العلم في أوّل سطر قبل أي شرط، كما في تهيئة الإشعارات — فلو خرجت الدالّة مبكراً لسبب بيئي لا تُعاد المحاولة. وإن كان الهدف «نفّذ مرّة واحدة بعد أن تتحقّق الشروط» فارفعه بعد الشروط وقبل التنفيذ مباشرة، كما في حقن الكود الخارجي. والخلط بين الحالتين يعطي إمّا عملية لا تعمل أبداً وإمّا عملية تعمل مرّتين.
وحالة ثالثة لطيفة: أحياناً لا تريد علماً بل مستمعاً يعمل مرّة ثم يزول بنفسه. إن كانت الطبقة السحابية غير جاهزة بعد، فبدل التجاهل الصامت يُسجَّل مستمع لحدث الجهوزية بالخيار { once: true }، فيُعيد المحاولة مرّة واحدة مضمونة ثم يفصل نفسه بلا حلقة انتظار ولا تنظيف يدوي.
أسئلة شائعة
هل أتعلّم JavaScript أم إطاراً مثل React مباشرة؟ أتقن الأساسيات أوّلاً. الأطر مبنيّة على JavaScript، ومن يقفز فوقها يتعثّر عند أوّل مشكلة خارج «وصفات» الإطار.
كم أحتاج لأبني شيئاً مفيداً؟ بعد إتقان ما في هذا الدليل تستطيع بناء نماذج تفاعلية، تحقّق من المدخلات، وجلب بيانات وعرضها. ابنِ مشاريع صغيرة حقيقية — هي أفضل معلّم.
أين أتمرّن؟ في وحدة تحكّم المتصفّح مباشرة، وعلى صفحة HTML بسيطة. لا تحتاج أدوات معقّدة للبداية، بل ملفاً واحداً وفضولاً.
هل السلسلة الاختيارية ?. تُبطئ الكود؟ لا، فحص القيمة الفارغة من أرخص ما ينفّذه المتصفّح، وتكلفتها الحقيقية في القراءة لا في الزمن. الخطر الوحيد أن تستعملها لتخفي بنية بيانات لا تفهمها؛ إن كان الحقل يجب أن يكون موجوداً دائماً فوجوده شرط تتحقّق منه صراحةً، لا تخطّيه بـ?..
هل أحتاج مكتبة جاهزة للتهريب؟ إن كنت تعرض نصّاً عادياً فدالّة الأحرف الخمسة أعلاه تكفي وتُقرأ في ثانية. لكن إن كنت تسمح للمستخدم بـHTML غنيّ (نصّ منسّق، روابط، صور) فالتهريب لا يكفي أصلاً، وتحتاج مكتبة تنقية تعمل بقائمة سماح للوسوم والسمات.
كيف أشخّص عيباً في الإنتاج وقد حُذفت رسائل console من الملف المصغَّر؟ بثلاث وسائل مرتّبة: أثر مرئي مقصود في الواجهة عند الحالات المهمّة، وحالة تنشرها على window بنفسك لتقرأها من الطرفية، وسجل الشبكة الذي يبقى كاملاً. وإن احتجت أكثر من ذلك فشغّل نسخة غير مصغَّرة محلياً وأعِد المشهد نفسه.
خلاصة
ابدأ بـconst/let، أتقن الدوال والمصفوفات والكائنات، تعلّم انتقاء عناصر DOM والاستماع للأحداث، ثم اكسر حاجز الكود غير المتزامن بـasync/await. هذه الأساسيات تغطّي معظم ما تحتاجه في مشاريعك الأولى.
لكن الدرس الأهم من قراءة كود يعمل فعلاً هو أنّ الفرق بين «كود يشتغل عندي» و«كود يشتغل عند الناس» ليس في المزيد من الميزات، بل في أربع عادات صغيرة: لا تفترض وجود ما لم تفحصه (?. في موضعها، و?? حين يكون الصفر جواباً مشروعاً)، وهرّب كل نصّ من مستخدم لحظة إدراجه في الصفحة، وأعطِ كل عملية شبكة مساراً معلوماً للفشل بدل التفاؤل، واكتب سطر الإغلاق لحظة كتابة سطر الفتح — لكل مستمع دالّة إلغاء، ولكل مؤقّت شرط خروج، ولكل تهيئة حارس مرّة واحدة. هذه العادات لا تُتعلّم من نظرية، بل من عيب صامت أضاع عليك ساعة مرّة واحدة.