أساسيات JavaScript

تحديد عناصر DOM: getElementById و querySelector

45 دقيقة الدرس 21 من 60

ما هو DOM؟

DOM هو اختصار لـ Document Object Model (نموذج كائن المستند). عندما يقوم المتصفح بتحميل صفحة HTML، فإنه لا يعرض النص الخام ببساطة. بدلا من ذلك، يقوم بتحليل HTML وإنشاء بنية بيانات شجرية في الذاكرة تسمى DOM. يصبح كل عنصر HTML عقدة في هذه الشجرة، وكل عقدة هي كائن JavaScript له خصائص وطرق يمكنك قراءتها وتعديلها. DOM هو الجسر بين مستند HTML و JavaScript -- فهو ما يسمح لـ JavaScript بالتفاعل مع محتوى صفحة الويب وبنيتها وأسلوبها والتحكم فيها.

فكر في DOM كتمثيل حي لمستند HTML الخاص بك. بينما ملف HTML هو نص ثابت مخزن على الخادم، فإن DOM هو كائن ديناميكي موجود في ذاكرة المتصفح. عندما تغير DOM باستخدام JavaScript، يقوم المتصفح فورا بتحديث ما يراه المستخدم على الشاشة. هذا هو أساس كل تطبيق ويب تفاعلي، من القوائم المنسدلة البسيطة إلى تطبيقات الصفحة الواحدة المعقدة.

بنية شجرة DOM

ينظم DOM مستندك كشجرة هرمية. في أعلى القمة يوجد كائن document الذي يمثل صفحة HTML بأكملها. أسفله يقع عنصر <html> الذي يتفرع إلى <head> و <body>. كل عنصر متداخل داخل عنصر آخر يصبح عقدة فرعية، وكل عنصر يحتوي على عناصر أخرى يصبح عقدة أبوية. العناصر على نفس مستوى التداخل تسمى عقدا شقيقة.

مثال: مصدر HTML وشجرة DOM الخاصة به

<!DOCTYPE html>
<html>
<head>
    <title>صفحتي</title>
</head>
<body>
    <div id="container">
        <h1 class="title">مرحبا بالعالم</h1>
        <p class="description">مرحبا بك في صفحتي.</p>
        <ul id="nav-list">
            <li class="nav-item">الرئيسية</li>
            <li class="nav-item">من نحن</li>
            <li class="nav-item">اتصل بنا</li>
        </ul>
    </div>
</body>
</html>

// تصور شجرة DOM:
// document
//   └── html
//       ├── head
//       │   └── title ("صفحتي")
//       └── body
//           └── div#container
//               ├── h1.title ("مرحبا بالعالم")
//               ├── p.description ("مرحبا بك في صفحتي.")
//               └── ul#nav-list
//                   ├── li.nav-item ("الرئيسية")
//                   ├── li.nav-item ("من نحن")
//                   └── li.nav-item ("اتصل بنا")
ملاحظة: المحتوى النصي داخل العناصر يصبح أيضا عقدة في شجرة DOM -- وتحديدا عقدة نصية. لذا النص "مرحبا بالعالم" داخل <h1> هو عقدة خاصة به، منفصلة عن عقدة عنصر <h1>. التعليقات في HTML تصبح أيضا عقد تعليقات. فهم أنواع العقد المختلفة هذه مهم للتعامل المتقدم مع DOM.

كائن document

كائن document هو نقطة الدخول إلى DOM. إنه كائن عام متاح في أي كود JavaScript يعمل في المتصفح. كل طريقة تستخدمها للبحث عن العناصر أو إنشائها أو تعديلها تبدأ بـ document. يوفر خصائص مثل document.title (عنوان الصفحة) و document.body (عنصر body) و document.head (عنصر head) و document.documentElement (عنصر <html> الجذري). والأهم من ذلك، يوفر الطرق التي ستستخدمها لتحديد العناصر من الصفحة.

مثال: استكشاف كائن document

// الوصول إلى عنوان المستند
console.log(document.title); // "صفحتي"

// الوصول إلى عنصر body مباشرة
console.log(document.body); // <body>...</body>

// الوصول إلى عنصر html الجذري
console.log(document.documentElement); // <html>...</html>

// التحقق من عدد العناصر في body
console.log(document.body.children.length);

// الوصول إلى جميع الروابط في المستند
console.log(document.links);

// الوصول إلى جميع النماذج في المستند
console.log(document.forms);

getElementById -- التحديد بواسطة المعرف

طريقة getElementById() هي الأسرع والأكثر مباشرة لتحديد عنصر واحد من DOM. تبحث في المستند بأكمله عن عنصر يحمل سمة id المحددة وتعيد ذلك العنصر ككائن. بما أن المعرفات يجب أن تكون فريدة داخل مستند HTML، فإن هذه الطريقة تعيد دائما إما عنصرا واحدا أو null إذا لم يوجد عنصر بهذا المعرف.

مثال: استخدام getElementById

// تحديد div الحاوية
const container = document.getElementById('container');
console.log(container); // <div id="container">...</div>

// تحديد قائمة التنقل
const navList = document.getElementById('nav-list');
console.log(navList); // <ul id="nav-list">...</ul>

// محاولة تحديد عنصر غير موجود
const sidebar = document.getElementById('sidebar');
console.log(sidebar); // null

// تحقق دائما من وجود العنصر قبل استخدامه
const header = document.getElementById('main-header');
if (header) {
    console.log('تم العثور على الرأس:', header.textContent);
} else {
    console.log('لم يتم العثور على عنصر الرأس');
}
خطأ شائع: تمرير رمز الهاش # إلى getElementById(). على عكس محددات CSS، تأخذ هذه الطريقة سلسلة المعرف العادية فقط. كتابة document.getElementById('#container') ستعيد null لأنه لا يوجد عنصر معرفه حرفيا "#container". الاستدعاء الصحيح هو document.getElementById('container').

getElementsByClassName -- التحديد بواسطة اسم الفئة

طريقة getElementsByClassName() تحدد جميع العناصر التي تحمل فئة CSS محددة. على عكس getElementById() التي تعيد عنصرا واحدا، تعيد هذه الطريقة HTMLCollection -- كائن حي يشبه المصفوفة يحتوي على جميع العناصر المطابقة. يمكنك تمرير اسم فئة واحد أو أسماء فئات متعددة مفصولة بمسافات، وفي هذه الحالة يتم إرجاع العناصر التي تحمل جميع الفئات المحددة فقط.

مثال: استخدام getElementsByClassName

// تحديد جميع العناصر ذات الفئة "nav-item"
const navItems = document.getElementsByClassName('nav-item');
console.log(navItems); // HTMLCollection(3) [li.nav-item, li.nav-item, li.nav-item]
console.log(navItems.length); // 3

// الوصول إلى العناصر الفردية بالفهرس
console.log(navItems[0].textContent); // "الرئيسية"
console.log(navItems[1].textContent); // "من نحن"
console.log(navItems[2].textContent); // "اتصل بنا"

// التكرار عبر المجموعة باستخدام حلقة for
for (let i = 0; i < navItems.length; i++) {
    console.log(navItems[i].textContent);
}

// تحديد العناصر ذات فئات متعددة
// سيتم إرجاع العناصر التي تحمل كلتا الفئتين فقط
// <div class="card featured"> مطابق، <div class="card"> غير مطابق
const featuredCards = document.getElementsByClassName('card featured');

// يمكنك أيضا استدعاؤها على عنصر أبوي محدد
const container = document.getElementById('container');
const itemsInContainer = container.getElementsByClassName('nav-item');
console.log(itemsInContainer.length); // 3

getElementsByTagName -- التحديد بواسطة اسم الوسم

طريقة getElementsByTagName() تحدد جميع العناصر بالاسم المحدد لوسم HTML. مثل getElementsByClassName()، تعيد HTMLCollection حية. اسم الوسم غير حساس لحالة الأحرف، لذا 'div' و 'DIV' و 'Div' جميعها تعيد نفس النتائج. يمكنك أيضا تمرير '*' لتحديد جميع العناصر في المستند.

مثال: استخدام getElementsByTagName

// تحديد جميع عناصر القائمة في الصفحة
const listItems = document.getElementsByTagName('li');
console.log(listItems.length); // 3

// تحديد جميع الفقرات
const paragraphs = document.getElementsByTagName('p');
for (let i = 0; i < paragraphs.length; i++) {
    console.log(paragraphs[i].textContent);
}

// تحديد جميع العناصر (مفيد للعد)
const allElements = document.getElementsByTagName('*');
console.log('إجمالي العناصر:', allElements.length);

// التحديد داخل عنصر أبوي محدد
const navList = document.getElementById('nav-list');
const navListItems = navList.getElementsByTagName('li');
console.log(navListItems.length); // 3 (فقط عناصر li داخل #nav-list)

querySelector -- قوة محددات CSS

طريقة querySelector() هي أكثر طرق التحديد تنوعا المتاحة. تقبل أي سلسلة محدد CSS صالحة وتعيد أول عنصر مطابق. إذا لم يتطابق أي عنصر، تعيد null. هذه الطريقة تجلب القوة الكاملة لمحددات CSS إلى JavaScript -- يمكنك التحديد بالمعرف أو الفئة أو اسم الوسم أو السمة أو الفئة الزائفة أو أي مزيج من هذه باستخدام نفس المحددات التي تستخدمها في أوراق أنماط CSS الخاصة بك.

مثال: استخدام querySelector مع محددات متنوعة

// التحديد بالمعرف (يعادل getElementById)
const container = document.querySelector('#container');

// التحديد بالفئة (يعيد العنصر المطابق الأول فقط)
const firstNavItem = document.querySelector('.nav-item');
console.log(firstNavItem.textContent); // "الرئيسية"

// التحديد باسم الوسم (يعيد العنصر المطابق الأول)
const firstParagraph = document.querySelector('p');

// التحديد بالسمة
const emailInput = document.querySelector('input[type="email"]');
const dataElement = document.querySelector('[data-role="admin"]');

// التحديد بمحدد السليل
const containerTitle = document.querySelector('#container .title');

// التحديد بمحدد الابن المباشر
const directChild = document.querySelector('#container > h1');

// تحديد أول عنصر قائمة داخل قائمة محددة
const firstItem = document.querySelector('#nav-list li');

// التحديد باستخدام الفئات الزائفة
const firstChild = document.querySelector('ul li:first-child');
const lastChild = document.querySelector('ul li:last-child');
const thirdChild = document.querySelector('ul li:nth-child(3)');

// المحددات المعقدة تعمل أيضا
const activeLink = document.querySelector('nav a.active[href^="/ar"]');
نصيحة احترافية: عندما تحتاج إلى تحديد عنصر واحد، يفضل عموما استخدام querySelector() بدلا من getElementById() بسبب مرونتها. ومع ذلك، فإن getElementById() أسرع قليلا في الأداء لأن المتصفح يحسن عمليات البحث بالمعرف داخليا. بالنسبة لمعظم التطبيقات، الفرق لا يكاد يذكر، لكن في الكود الحساس للأداء الذي يعمل آلاف المرات في الثانية (مثل حلقات الألعاب)، فضل getElementById() للتحديدات المعتمدة على المعرف.

querySelectorAll -- تحديد جميع المطابقات

طريقة querySelectorAll() تعمل مثل querySelector() لكن بدلا من إرجاع المطابقة الأولى فقط، تعيد NodeList تحتوي على جميع العناصر المطابقة لمحدد CSS. إذا لم تتطابق أي عناصر، تعيد NodeList فارغة (وليس null). هذه الطريقة قوية جدا للعمليات المجمعة على مجموعات من العناصر.

مثال: استخدام querySelectorAll

// تحديد جميع عناصر التنقل
const allNavItems = document.querySelectorAll('.nav-item');
console.log(allNavItems.length); // 3

// querySelectorAll تعيد NodeList التي تدعم forEach
allNavItems.forEach(function(item) {
    console.log(item.textContent);
});

// تحديد جميع الفقرات داخل الحاوية
const containerParagraphs = document.querySelectorAll('#container p');

// تحديد عناصر مختلفة متعددة بمحددات مفصولة بفواصل
const headingsAndParagraphs = document.querySelectorAll('h1, h2, h3, p');

// تحديد جميع حقول الإدخال المطلوبة
const requiredFields = document.querySelectorAll('input[required]');

// تحديد جميع عناصر القائمة الزوجية باستخدام nth-child
const evenItems = document.querySelectorAll('li:nth-child(even)');

// تحديد جميع الروابط التي تفتح في علامة تبويب جديدة
const externalLinks = document.querySelectorAll('a[target="_blank"]');

// تحويل NodeList إلى مصفوفة حقيقية إذا كنت تحتاج طرق المصفوفة
const itemsArray = Array.from(allNavItems);
// أو باستخدام عامل الانتشار
const itemsArray2 = [...allNavItems];

NodeList مقابل HTMLCollection: فرق جوهري

فهم الفرق بين NodeList و HTMLCollection أمر ضروري لتجنب الأخطاء الدقيقة. نوعا المجموعتين هذان يبدوان متشابهين لكنهما يتصرفان بشكل مختلف بطرق مهمة.

HTMLCollection يتم إرجاعها بواسطة getElementsByClassName() و getElementsByTagName(). إنها مجموعة حية، بمعنى أنها تتحدث تلقائيا عندما يتغير DOM. إذا أضفت أو أزلت عناصر تطابق المعايير، يتغير طول المجموعة ومحتوياتها في الوقت الفعلي. HTMLCollection لا تملك طريقة forEach()، لذا يجب استخدام حلقة for تقليدية أو تحويلها إلى مصفوفة.

NodeList المعادة بواسطة querySelectorAll() هي مجموعة ثابتة. إنها لقطة من DOM في اللحظة التي استدعيت فيها الطريقة. إضافة أو إزالة العناصر بعد التحديد لا يغير NodeList. NodeList تملك طريقة forEach()، مما يجعل التكرار عليها أسهل.

مثال: HTMLCollection الحية مقابل NodeList الثابتة

// الإعداد: <ul id="list"><li>أ</li><li>ب</li></ul>

// HTMLCollection حية
const liveItems = document.getElementsByTagName('li');
console.log(liveItems.length); // 2

// NodeList ثابتة
const staticItems = document.querySelectorAll('li');
console.log(staticItems.length); // 2

// الآن أضف عنصر قائمة جديد إلى DOM
const newItem = document.createElement('li');
newItem.textContent = 'ج';
document.getElementById('list').appendChild(newItem);

// HTMLCollection الحية تحدثت تلقائيا!
console.log(liveItems.length); // 3

// NodeList الثابتة لم تتحدث
console.log(staticItems.length); // 2 (لا تزال اللقطة الأصلية)

// HTMLCollection لا تملك forEach
// liveItems.forEach(...) سيرمي خطأ!

// لكن يمكنك تحويلها إلى مصفوفة أولا
Array.from(liveItems).forEach(function(item) {
    console.log(item.textContent);
});

// NodeList تملك forEach
staticItems.forEach(function(item) {
    console.log(item.textContent);
});
خطأ شائع: تعديل DOM أثناء التكرار على HTMLCollection حية يمكن أن يؤدي إلى تخطي عناصر أو حلقات لا نهائية. على سبيل المثال، إذا كررت على مجموعة حية وأزلت عناصر، تتقلص المجموعة أثناء التكرار، مما يتسبب في تخطي كل عنصر ثانٍ. قم دائما بتحويل المجموعة الحية إلى مصفوفة باستخدام Array.from() قبل تعديل DOM أثناء التكرار.

التحديد بواسطة السمات

محددات سمات CSS تعمل في querySelector() و querySelectorAll()، مما يمنحك تحكما دقيقا في العناصر التي تحددها. يمكنك مطابقة العناصر بوجود سمة أو قيمتها الدقيقة أو حتى قيم جزئية باستخدام عوامل مختلفة. هذا مفيد بشكل خاص لتحديد عناصر النماذج وسمات البيانات وسمات ARIA.

مثال: التحديد المبني على السمات

// التحديد بوجود سمة
const elementsWithTitle = document.querySelectorAll('[title]');

// التحديد بقيمة سمة دقيقة
const submitBtn = document.querySelector('button[type="submit"]');
const checkbox = document.querySelector('input[type="checkbox"]');

// التحديد بقيمة سمة تبدأ بسلسلة (^=)
const internalLinks = document.querySelectorAll('a[href^="/ar"]');

// التحديد بقيمة سمة تنتهي بسلسلة ($=)
const pdfLinks = document.querySelectorAll('a[href$=".pdf"]');

// التحديد بقيمة سمة تحتوي على سلسلة (*=)
const searchInputs = document.querySelectorAll('input[name*="search"]');

// التحديد بسمات البيانات
const adminPanels = document.querySelectorAll('[data-role="admin"]');
const activeSlides = document.querySelectorAll('[data-active="true"]');

// دمج محددات السمات مع محددات أخرى
const requiredEmailField = document.querySelector(
    'form.login input[type="email"][required]'
);

// تحديد سمات ARIA لاختبار إمكانية الوصول
const expandedMenus = document.querySelectorAll('[aria-expanded="true"]');
const hiddenElements = document.querySelectorAll('[aria-hidden="true"]');

closest() -- البحث صعودا في شجرة DOM

بينما querySelector() تبحث نزولا عبر أحفاد العنصر، طريقة closest() تبحث صعودا عبر أسلاف العنصر. تبدأ من العنصر نفسه وتصعد في سلسلة العناصر الأبوية، وتعيد أول سلف يطابق محدد CSS المعطى. إذا لم يتطابق أي سلف، تعيد null. هذه الطريقة مفيدة للغاية في التعامل مع الأحداث عندما تحتاج للعثور على حاوية أبوية من عنصر فرعي تم النقر عليه.

مثال: استخدام closest() للتنقل صعودا في DOM

// HTML:
// <div class="card" data-id="42">
//   <div class="card-body">
//     <h3 class="card-title">اسم المنتج</h3>
//     <p class="card-text">الوصف هنا...</p>
//     <button class="btn-delete">حذف</button>
//   </div>
// </div>

const deleteBtn = document.querySelector('.btn-delete');

// العثور على أقرب عنصر أبوي بفئة "card"
const card = deleteBtn.closest('.card');
console.log(card); // <div class="card" data-id="42">...</div>

// الحصول على البيانات من البطاقة
const cardId = card.dataset.id;
console.log(cardId); // "42"

// العثور على أقرب عنصر أبوي بفئة "card-body"
const cardBody = deleteBtn.closest('.card-body');
console.log(cardBody); // <div class="card-body">...</div>

// closest() تفحص العنصر نفسه أولا
const selfCheck = card.closest('.card');
console.log(selfCheck === card); // true (طابقت نفسها!)

// تعيد null إذا لم يتطابق أي سلف
const table = deleteBtn.closest('table');
console.log(table); // null

// واقعي: نمط تفويض الأحداث
document.addEventListener('click', function(event) {
    const card = event.target.closest('.card');
    if (card) {
        console.log('تم النقر داخل البطاقة:', card.dataset.id);
    }
});

matches() -- اختبار ما إذا كان العنصر يطابق محددا

طريقة matches() تتيح لك التحقق مما إذا كان العنصر يطابق محدد CSS معين. تعيد true أو false ولا تبحث في DOM -- إنها ببساطة تختبر العنصر الذي تستدعيها عليه. هذا مفيد في المنطق الشرطي، خاصة عند دمجه مع تفويض الأحداث حيث تحتاج للتحقق من أن العنصر الذي تم النقر عليه يحمل فئة أو سمة معينة.

مثال: استخدام matches() لاختبار العناصر

const heading = document.querySelector('h1.title');

// اختبار ما إذا كان العنصر يطابق محددات مختلفة
console.log(heading.matches('h1')); // true
console.log(heading.matches('.title')); // true
console.log(heading.matches('h1.title')); // true
console.log(heading.matches('h2')); // false
console.log(heading.matches('.subtitle')); // false
console.log(heading.matches('#container h1')); // true (يتحقق من السياق أيضا)

// مفيد في تفويض الأحداث
document.addEventListener('click', function(event) {
    if (event.target.matches('.btn-delete')) {
        console.log('تم النقر على زر الحذف');
    }
    if (event.target.matches('a[href^="http"]')) {
        console.log('تم النقر على رابط خارجي');
    }
    if (event.target.matches('.nav-item, .nav-link')) {
        console.log('تم النقر على عنصر تنقل');
    }
});

أداء المحددات المختلفة

ليست جميع طرق تحديد DOM متساوية في الأداء. عندما تبني تطبيقات ويب معقدة تستعلم DOM بشكل متكرر، فهم الأداء النسبي يمكن أن يساعدك في اتخاذ خيارات أفضل. إليك الترتيب العام من الأسرع إلى الأبطأ:

  1. getElementById() -- الأسرع لأن المتصفحات تحتفظ بخريطة تجزئة داخلية للمعرفات للبحث الفوري.
  2. getElementsByClassName() -- سريعة جدا لأن عمليات البحث بالفئة محسنة داخليا.
  3. getElementsByTagName() -- سريعة لأن أسماء الوسوم جزء من البنية الأساسية للعنصر.
  4. querySelector() -- أبطأ قليلا لأن المتصفح يجب أن يحلل سلسلة محدد CSS قبل البحث.
  5. querySelectorAll() -- الأبطأ لأنها يجب أن تجد جميع المطابقات وتبني NodeList كاملة، لكن الفرق نادرا ما يكون ملحوظا.

مع ذلك، فرق الأداء بين هذه الطرق يقاس عادة بالميكروثانية. بالنسبة للغالبية العظمى من صفحات وتطبيقات الويب، يجب أن تختار الطريقة الأكثر قابلية للقراءة والأنسب لحالة استخدامك بدلا من التحسين الدقيق لأداء المحددات. القابلية للقراءة والصيانة أكثر قيمة بكثير من توفير بضع ميكروثوانٍ لكل استعلام DOM.

مثال: مقارنة الأداء

// قياس أداء التحديد
console.time('getElementById');
for (let i = 0; i < 100000; i++) {
    document.getElementById('container');
}
console.timeEnd('getElementById');
// عادة: ~5-15 ميلي ثانية لـ 100,000 عملية بحث

console.time('querySelector بالمعرف');
for (let i = 0; i < 100000; i++) {
    document.querySelector('#container');
}
console.timeEnd('querySelector بالمعرف');
// عادة: ~15-40 ميلي ثانية لـ 100,000 عملية بحث

console.time('querySelector معقد');
for (let i = 0; i < 100000; i++) {
    document.querySelector('#container > .title');
}
console.timeEnd('querySelector معقد');
// عادة: ~25-60 ميلي ثانية لـ 100,000 عملية بحث

// الفرق لكل استدعاء واحد لا يكاد يذكر
// ركز على القابلية للقراءة بدلا من التحسين الدقيق

تخزين مراجع DOM مؤقتا

أحد أكثر تحسينات الأداء تأثيرا التي يمكنك القيام بها هو تخزين مراجع DOM مؤقتا. في كل مرة تستدعي فيها طريقة تحديد، يجب على المتصفح البحث في شجرة DOM للعثور على العناصر المطابقة. إذا استخدمت نفس العنصر عدة مرات، خزن المرجع في متغير بدلا من إعادة الاستعلام من DOM في كل مرة. هذا مهم بشكل خاص داخل الحلقات ومعالجات الأحداث ودوال الرسوم المتحركة التي تنفذ بشكل متكرر.

مثال: تخزين مراجع DOM مؤقتا

// سيئ: الاستعلام من DOM بشكل متكرر
function updateUI() {
    document.querySelector('.status').textContent = 'جار التحميل...';
    document.querySelector('.status').style.color = 'orange';
    document.querySelector('.status').classList.add('active');
    // المتصفح يبحث في DOM 3 مرات منفصلة!
}

// جيد: تخزين المرجع في متغير
function updateUI() {
    const status = document.querySelector('.status');
    status.textContent = 'جار التحميل...';
    status.style.color = 'orange';
    status.classList.add('active');
    // المتصفح يبحث في DOM مرة واحدة فقط!
}

// تخزين المراجع في أعلى السكريبت للعناصر المستخدمة بكثرة
const elements = {
    header: document.getElementById('main-header'),
    nav: document.getElementById('main-nav'),
    content: document.getElementById('main-content'),
    footer: document.getElementById('main-footer'),
    sidebar: document.getElementById('sidebar'),
};

// استخدام المراجع المخزنة في جميع أنحاء الكود
elements.header.classList.add('sticky');
elements.sidebar.style.display = 'none';

// كن حذرا: إذا تمت إزالة عنصر DOM وإعادة إنشائه،
// يصبح مرجعك المخزن قديما (يشير إلى العنصر القديم).
// في مثل هذه الحالات، أعد الاستعلام من DOM عند الحاجة.
نصيحة احترافية: نمط شائع في الكود الاحترافي هو تعريف جميع مراجع DOM في أعلى ملف JavaScript أو الوحدة، بشكل مشابه لكيفية استيراد التبعيات. هذا يجعل من الواضح فورا العناصر التي يعتمد عليها كودك ويمنع استعلامات DOM المتكررة المنتشرة في منطقك البرمجي.

تحديد العناصر ضمن سياق محدد

لا يجب دائما البحث في المستند بأكمله. يمكن استدعاء كل من querySelector() و querySelectorAll() على أي عنصر، وليس فقط document. عند استدعائهما على عنصر محدد، يبحثان فقط داخل أحفاد ذلك العنصر. هذا يضيق نطاق البحث ويحسن الأداء ويمنع التحديد العرضي لعناصر خارج السياق المقصود.

مثال: التحديد المحصور داخل عنصر أبوي

// HTML:
// <div id="section-a">
//   <p class="intro">مقدمة القسم أ</p>
//   <p class="detail">تفاصيل القسم أ</p>
// </div>
// <div id="section-b">
//   <p class="intro">مقدمة القسم ب</p>
//   <p class="detail">تفاصيل القسم ب</p>
// </div>

// البحث داخل قسم محدد
const sectionA = document.getElementById('section-a');
const sectionB = document.getElementById('section-b');

// هذه تبحث فقط داخل أقسامها المعنية
const introA = sectionA.querySelector('.intro');
console.log(introA.textContent); // "مقدمة القسم أ"

const introB = sectionB.querySelector('.intro');
console.log(introB.textContent); // "مقدمة القسم ب"

// تحديد جميع الفقرات داخل القسم أ فقط
const parasInA = sectionA.querySelectorAll('p');
console.log(parasInA.length); // 2 (فقط الموجودة داخل section-a)

// هذا مفيد جدا لأنماط المكونات
function initializeCard(cardElement) {
    const title = cardElement.querySelector('.card-title');
    const body = cardElement.querySelector('.card-body');
    const btn = cardElement.querySelector('.card-btn');
    // جميع التحديدات محصورة في هذه البطاقة المحددة
}

مثال واقعي: بناء نظام تنقل بالعلامات

لنجمع جميع طرق التحديد معا في مثال عملي. سنبني نظام تنقل بالعلامات حيث يؤدي النقر على زر علامة إلى عرض لوحة المحتوى المقابلة. هذا المثال يوضح تحديد المعرف وتحديد الفئة و querySelectorAll و forEach و closest() و matches() وتخزين مراجع DOM مؤقتا -- كلها تعمل معا في سيناريو واقعي.

مثال: تنقل العلامات مع تحديد DOM

// بنية HTML:
// <div class="tabs-container" id="product-tabs">
//   <div class="tab-buttons">
//     <button class="tab-btn active" data-tab="overview">نظرة عامة</button>
//     <button class="tab-btn" data-tab="specs">المواصفات</button>
//     <button class="tab-btn" data-tab="reviews">المراجعات</button>
//   </div>
//   <div class="tab-panels">
//     <div class="tab-panel active" id="panel-overview">محتوى النظرة العامة...</div>
//     <div class="tab-panel" id="panel-specs">محتوى المواصفات...</div>
//     <div class="tab-panel" id="panel-reviews">محتوى المراجعات...</div>
//   </div>
// </div>

// تخزين مراجع DOM مؤقتا
const tabContainer = document.getElementById('product-tabs');
const tabButtons = tabContainer.querySelectorAll('.tab-btn');
const tabPanels = tabContainer.querySelectorAll('.tab-panel');

// دالة لتبديل العلامات
function switchTab(targetTabName) {
    // إلغاء تنشيط جميع الأزرار
    tabButtons.forEach(function(btn) {
        btn.classList.remove('active');
    });

    // إلغاء تنشيط جميع اللوحات
    tabPanels.forEach(function(panel) {
        panel.classList.remove('active');
    });

    // تنشيط الزر الذي تم النقر عليه
    const activeButton = tabContainer.querySelector(
        '.tab-btn[data-tab="' + targetTabName + '"]'
    );
    activeButton.classList.add('active');

    // تنشيط اللوحة المقابلة
    const activePanel = document.getElementById('panel-' + targetTabName);
    activePanel.classList.add('active');
}

// استخدام تفويض الأحداث على الحاوية (أفضل ممارسة)
tabContainer.addEventListener('click', function(event) {
    // التحقق مما إذا تم النقر على زر علامة باستخدام closest()
    const clickedBtn = event.target.closest('.tab-btn');
    if (!clickedBtn) return; // النقر لم يكن على زر علامة

    // الحصول على اسم العلامة المستهدفة من سمة البيانات
    const tabName = clickedBtn.dataset.tab;
    switchTab(tabName);
});

مثال واقعي: التحقق من النماذج مع تحديد DOM

حالة استخدام شائعة أخرى لتحديد DOM هي التحقق من النماذج. يوضح هذا المثال كيف يمكنك دمج طرق تحديد متعددة للتحقق من نموذج تسجيل، وتحديد حقول إدخال محددة بالنوع، والتحقق من الحقول المطلوبة، وعرض رسائل الخطأ بتحديد حاويات الخطأ المقابلة.

مثال: التحقق من النماذج

// تخزين النموذج وجميع الحقول المطلوبة مؤقتا
const form = document.getElementById('registration-form');
const requiredFields = form.querySelectorAll('[required]');
const emailField = form.querySelector('input[type="email"]');
const passwordField = form.querySelector('input[type="password"]');
const submitBtn = form.querySelector('button[type="submit"]');

function validateField(field) {
    // العثور على حاوية رسالة الخطأ بجوار هذا الحقل
    const errorContainer = field.closest('.form-group')
                               .querySelector('.error-message');

    if (!field.value.trim()) {
        errorContainer.textContent = 'هذا الحقل مطلوب';
        field.classList.add('error');
        return false;
    }

    // التحقق من تنسيق البريد الإلكتروني
    if (field.matches('input[type="email"]') &&
        !field.value.includes('@')) {
        errorContainer.textContent = 'يرجى إدخال بريد إلكتروني صحيح';
        field.classList.add('error');
        return false;
    }

    // مسح الأخطاء
    errorContainer.textContent = '';
    field.classList.remove('error');
    return true;
}

// التحقق من جميع الحقول المطلوبة عند إرسال النموذج
form.addEventListener('submit', function(event) {
    let isValid = true;

    requiredFields.forEach(function(field) {
        if (!validateField(field)) {
            isValid = false;
        }
    });

    if (!isValid) {
        event.preventDefault();
        // التمرير إلى أول خطأ
        const firstError = form.querySelector('.error');
        if (firstError) {
            firstError.focus();
        }
    }
});

ملخص طرق التحديد

إليك مرجعا سريعا لجميع طرق تحديد DOM التي تم تغطيتها في هذا الدرس:

  • document.getElementById(id) -- تعيد عنصرا واحدا بمعرفه الفريد. أسرع طريقة تحديد.
  • document.getElementsByClassName(className) -- تعيد HTMLCollection حية لجميع العناصر بالفئة المحددة.
  • document.getElementsByTagName(tagName) -- تعيد HTMLCollection حية لجميع العناصر باسم الوسم المحدد.
  • document.querySelector(selector) -- تعيد أول عنصر يطابق أي محدد CSS صالح.
  • document.querySelectorAll(selector) -- تعيد NodeList ثابتة لجميع العناصر المطابقة لأي محدد CSS صالح.
  • element.closest(selector) -- تبحث صعودا في شجرة DOM وتعيد أول سلف يطابق المحدد.
  • element.matches(selector) -- تعيد true أو false للإشارة إلى ما إذا كان العنصر يطابق المحدد.

تمرين عملي

أنشئ صفحة HTML بالبنية التالية: شريط تنقل يحتوي على خمسة روابط (كل منها بفئة nav-link وسمة data-section)، وثلاثة أقسام محتوى (كل منها بمعرف فريد وفئة content-section)، وتذييل يحتوي على فقرة. ثم اكتب JavaScript ينجز المهام التالية: (1) استخدم getElementById() لتحديد قسم المحتوى الأول وسجل نصه في وحدة التحكم. (2) استخدم getElementsByClassName() لتحديد جميع روابط التنقل وسجل عددها. (3) استخدم querySelector() لتحديد أول رابط بقيمة data-section محددة. (4) استخدم querySelectorAll() لتحديد جميع أقسام المحتوى والتكرار عليها باستخدام forEach(). (5) أظهر الفرق بين HTMLCollection الحية و NodeList الثابتة بإضافة عنصر جديد والتحقق من طول كل منهما. (6) استخدم closest() من عنصر متداخل بعمق للعثور على قسمه الأبوي. (7) خزن جميع مراجع DOM المستخدمة بكثرة في كائن في أعلى السكريبت. اختبر كل طريقة وتحقق من النتائج في وحدة تحكم المتصفح.