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

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

الـ DOM: التحديد والإنشاء والتحديث

18 دقراءة 3 دبيئة تجريب الكود

الدرس في لمحة

تفهم الـ DOM على أنه شجرة الصفحة الحيّة في المتصفّح، وتحدّد العناصر بمحدّدات CSS، وتغيّر نصوصها وأصنافها وسماتها، وتعرض قائمة انطلاقًا من البيانات.

  1. 1الـ DOM: الصفحة على شكل كائنات
  2. 2تحديد العناصر
  3. 3تغيير ما هو موجود
  4. 4إنشاء العناصر وعرض قائمة

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

كل ما فعله Pocket حتى الآن حدث في الـ console. لكن المستخدم الحقيقي لا يفتح الـ console أبدًا؛ إنه يرى صفحة. وباقي هذا القسم ينقل Pocket إلى تلك الصفحة، وأول خطوة أن تتعلّم كيف ترى JavaScript صفحة الويب أصلًا.

الـ DOM: الصفحة على شكل كائنات

حين يحمّل المتصفّح HTML، لا يحتفظ به نصًا. بل يبني شجرة من الكائنات، كائنًا لكل عنصر، ويرسم الصفحة من تلك الشجرة. هذه الشجرة هي الـ DOM (نموذج كائنات المستند، Document Object Model)، والكائن العام document هو جذرها.

index.html
<body>
  <h1>Pocket</h1>
  <p id="total">Total: $0.00</p>
  <ul id="expense-list"></ul>
  <script src="app.js" defer></script>
</body>
شجرة الـ DOM لصفحة Pocket: يحتوي document على body، ويحتوي body على h1، وعلى p معرّفه total، وعلى ul معرّفه expense-list. تقرأ JavaScript هذه الكائنات وتغيّرها، فيعيد المتصفّح رسم الصفحة. document body h1 p#total ul#expense-list JavaScript تغيّر هذه
يحوّل المتصفّح HTML إلى شجرة من كائنات العناصر

الـ DOM حيّ. غيّر كائنًا في الشجرة فيعيد المتصفّح رسم ذلك الجزء من الصفحة على الفور. وهذا كل ما يعنيه “تحديث الصفحة”.

السمة defer تخبر المتصفّح أن يشغّل app.js بعد أن يبني الشجرة كلها. ودونها، يُنفَّذ السكربت الموجود في <head> قبل أن يوجد الـ body، فلا يجد أي بحث شيئًا. أما السكربتات المحمّلة بـ type="module"، التي ستتعرّف عليها في القسم 5، فتُؤجَّل تلقائيًا.

تحديد العناصر

document.querySelector(selector) تأخذ محدّد CSS، أي الصياغة نفسها التي تكتبها في ملف الأنماط، وتُرجع أول عنصر مطابق. وquerySelectorAll تُرجع كل العناصر المطابقة.

app.js
const totalEl = document.querySelector("#total");          // by id
const list = document.querySelector("#expense-list");
const firstItem = document.querySelector("#expense-list li");
const allItems = document.querySelectorAll("li.big");      // every match

querySelectorAll تُرجع NodeList. لها forEach وتعمل مع for...of؛ وانشرها، [...allItems]، حين تريد دوال المصفوفات مثل map وfilter.

حين لا يطابق شيء، تُرجع querySelector القيمة null، فيرمي السطر التالي الذي يستخدم النتيجة TypeError: Cannot set properties of null. وهذا الخطأ يعني في الغالب خطأً إملائيًا في المحدّد أو سكربتًا نُفّذ مبكرًا جدًا.

تغيير ما هو موجود

حين تحصل على عنصر، تصبح خصائصه أدوات التحكّم:

app.js
totalEl.textContent = "Total: $30.20";      // replace the text
totalEl.classList.add("over-budget");       // add a CSS class
totalEl.classList.toggle("highlight");      // add if absent, remove if present
totalEl.hidden = false;                     // show or hide
list.dataset.month = "2026-10";             // sets data-month="2026-10"

textContent تضبط نصًا عاديًا. وفضّل الأصناف (classes) على ضبط style مباشرة: فملف CSS يقرّر شكل .over-budget، وJavaScript تقرّر فقط هل يحمل العنصر هذا الصنف. وعندها يستطيع المصمّمون تغيير الشكل دون أن يمسّوا منطقك.

إنشاء العناصر وعرض قائمة

لتضيف أشياء جديدة إلى الصفحة، أنشئ العناصر، واملأها، ثم ألحقها بالصفحة:

app.js
const li = document.createElement("li");
li.textContent = "Coffee: $4.50";
li.dataset.id = "exp-1";
list.append(li);

createElement تصنع عنصرًا ليس على الصفحة بعد. وappend تُلحقه ابنًا أخيرًا لـ list، وعندها فقط يظهر. أما element.remove() فتُزيل عنصرًا من الصفحة.

لدى Pocket مصفوفة مصاريف ويحتاج إلى <li> لكل مصروف. وأنظف طريقة لإبقاء الصفحة والبيانات متّفقتين هي دالة render (دالة العرض): تُفرغ الحاوية، ثم تعيد بناءها من البيانات، في كل مرة تتغيّر فيها البيانات.

app.js
function renderExpenses(expenses) {
  list.replaceChildren(); // remove the old items
  for (const expense of expenses) {
    const li = document.createElement("li");
    li.textContent = `${expense.label}: ${formatMoney(expense.amount)}`;
    li.dataset.id = expense.id;
    if (expense.amount >= 10000) {
      li.classList.add("big");
    }
    list.append(li);
  }
  const total = expenses.reduce((sum, e) => sum + e.amount, 0);
  totalEl.textContent = `Total: ${formatMoney(total)}`;
}

إعادة بناء كل شيء تبدو هدرًا، وهي كذلك مع آلاف العناصر. لكن مع عشرات العناصر في تطبيق شخصي، فهي سريعة، والأهم أنها بسيطة: هناك دالة واحدة فقط تقرّر شكل القائمة، ولا يمكن للصفحة أن تخرج عن تزامنها مع البيانات. وهذه الفكرة، أن الشاشة دالةٌ في البيانات، هي جوهر أطر العمل مثل React، التي تفعل الشيء نفسه بذكاء أكبر.

والقاعدة التي تجعل هذا ينجح هي الانضباط في الاتجاه. البيانات تسير في اتجاه واحد: غيّر المصفوفة، ثم استدعِ renderExpenses. لا تُصلح عنصر <li> واحدًا يدويًا “هذه المرة فقط”، ولا تقرأ المصاريف من الصفحة لتحسب شيئًا؛ فالصفحة صورة للبيانات، لا البيانات نفسها. وفي اللحظة التي يصبح فيها مكانان قادرين على تغيير ما على الشاشة، سيختلفان حتمًا في يوم ما.

صارت الصفحة تعرض بيانات Pocket، لكنها لا تستطيع التفاعل مع المستخدم بعد. الدرس التالي: الأحداث.

أهم النقاط

  • يحوّل المتصفّح كود HTML إلى الـ DOM، وهو شجرة من الكائنات؛ وتغيير هذه الكائنات بـ JavaScript يغيّر الصفحة فورًا.
  • querySelector تُرجع أول عنصر يطابق محدّد CSS، أو null؛ وquerySelectorAll تُرجع كل العناصر المطابقة.
  • اضبط النص باستخدام textContent لا innerHTML كلما جاء المحتوى من البيانات أو من المستخدمين، كي لا يُنفَّذ أبدًا على أنه HTML.
  • غيّر المظهر عبر classList، واترك لـ CSS أن تقرّر شكل هذه الأصناف.
  • دالة render تُفرغ الحاوية وتعيد بناءها من بياناتك تُبقي الصفحة والبيانات متزامنتين.

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

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

السطر document.querySelector("#totl").textContent = "$0.00"; يرمي TypeError: Cannot set properties of null. ما السبب الأرجح؟

السؤال 2 من 3

يكتب مستخدم <img src=x onerror=alert(1)> تسميةً لمصروف. أيّ سطر يعرضها بأمان؟

السؤال 3 من 3

دالة render لديك تضيف <li> لكل مصروف. بعد إضافة مصروف رابع واستدعاء render مجددًا، تعرض القائمة سبعة عناصر. ما الناقص؟

تدريب

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

اعرض قائمة Pocket

اكتب جسم الدالة renderExpenses(expenses) بحيث تعيد بناء الصفحة من البيانات:

  1. أفرغ #expense-list.
  2. أضف <li> لكل مصروف نصّه مثل Coffee: $4.50 (التسمية، ثم نقطتان، ثم المبلغ منسّقًا)، واضبط data-id الخاص به على id المصروف، وأضف الصنف big حين يكون المبلغ 10000 سنت أو أكثر.
  3. اضبط #total على Total: متبوعًا بالمجموع منسّقًا.

يجب أن يستبدل استدعاء renderExpenses مرة أخرى القائمة لا أن يضيف إليها، ويجب أن تُعرض التسميات نصًا عاديًا.

<h1>Pocket</h1>
<p id="total">Total: $0.00</p>
<ul id="expense-list"></ul>
جارٍ تحميل المحرّر…

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

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • عنصر
  • واحد لكل مصروف، مثل “Coffee: $4.50”
  • كل عنصر يحمل معرّف مصروفه في data-id
  • المصاريف التي قيمتها 10000 سنت أو أكثر فقط تحصل على الصنف “big”
  • يعرض #total النص “Total: $207.00”
  • العرض مجددًا يستبدل القائمة ويعرض التسميات نصًا عاديًا