Tổng hợp DOM trong JavaScript

Tổng hợp DOM trong JavaScript từ A-Z

Khám phá toàn diện DOM trong JavaScript từ khái niệm, cấu trúc, cách truy cập phần tử, thao tác nội dung, thuộc tính, CSS, sự kiện, tạo và xóa phần tử đến các kỹ thuật tối ưu hiệu suất. Bài viết tổng hợp dễ hiểu, có ví dụ thực tế dành cho người mới và lập trình viên Front-end, chi tiết như nào hãy cùng chúng tôi phân tích:

DOM là một trong những kiến thức quan trọng nhất khi học JavaScript. Nếu JavaScript giúp tạo ra các chức năng động thì DOM chính là cầu nối để JavaScript tương tác trực tiếp với nội dung của trang web. Từ việc thay đổi văn bản, chỉnh sửa giao diện, xử lý sự kiện người dùng cho đến xây dựng các ứng dụng web hiện đại, tất cả đều dựa trên DOM.

Bài viết này được Web Mới tổng hợp theo hướng thực hành, giải thích dễ hiểu từ nền tảng đến nâng cao. Thay vì chỉ liệt kê các phương thức, nội dung sẽ giúp bạn hiểu bản chất hoạt động của DOM, biết khi nào nên sử dụng từng API và tránh những lỗi phổ biến trong quá trình phát triển website.

Tổng hợp DOM trong JavaScript

DOM là gì?

DOM (Document Object Model) là mô hình biểu diễn tài liệu HTML hoặc XML dưới dạng cây đối tượng. Khi trình duyệt tải một trang web, toàn bộ mã HTML sẽ được phân tích và chuyển thành các đối tượng để JavaScript có thể truy cập cũng như thao tác.

Hiểu đơn giản, mỗi phần tử trên trang như tiêu đề, đoạn văn, hình ảnh, nút bấm hay bảng đều trở thành một đối tượng trong cây DOM.

Ví dụ, đoạn HTML dưới đây:

<div class="box">
    <h2>Xin chào</h2>
    <p>Đây là nội dung.</p>
</div>

Sau khi được trình duyệt phân tích, cấu trúc DOM sẽ tương tự như:

  • Document
  • html
    • body
      • div
        • h2
        • p

Nhờ mô hình cây này, JavaScript có thể tìm đến bất kỳ phần tử nào và thực hiện các thao tác như đọc dữ liệu, chỉnh sửa nội dung hoặc thay đổi giao diện.

DOM hoạt động như thế nào?

Khi người dùng truy cập website, trình duyệt sẽ trải qua các bước cơ bản sau:

  1. Tải mã HTML từ máy chủ.
  2. Phân tích cú pháp HTML.
  3. Tạo cây DOM.
  4. Tạo CSSOM từ các tệp CSS.
  5. Kết hợp DOM và CSSOM để tạo Render Tree.
  6. Hiển thị giao diện lên màn hình.

JavaScript có thể can thiệp vào cây DOM ở bất kỳ thời điểm nào sau khi tài liệu được tải xong. Mỗi khi DOM thay đổi, trình duyệt sẽ cập nhật giao diện tương ứng.

Ví dụ:

document.querySelector("#title").textContent = "Web Mới";

Dòng lệnh trên sẽ tìm phần tử có id là title và thay đổi nội dung hiển thị thành "Web Mới". Người dùng sẽ nhìn thấy sự thay đổi ngay lập tức mà không cần tải lại trang.

Các thành phần trong cây DOM

Mỗi đối tượng trong DOM đều được gọi là một Node. Có nhiều loại Node khác nhau, nhưng khi lập trình JavaScript bạn sẽ thường xuyên làm việc với một số loại sau.

Document

Đây là đối tượng cao nhất của cây DOM. Mọi thao tác với trang web đều bắt đầu từ đối tượng document.

Ví dụ:

console.log(document.title);

Element

Element là các phần tử HTML như div, p, img, table, form, button...

Ví dụ:

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

Text Node

Là phần văn bản nằm bên trong các phần tử HTML.

Ví dụ:

<p>Xin chào JavaScript</p>

Chuỗi "Xin chào JavaScript" chính là một Text Node.

Attribute

Là các thuộc tính của phần tử HTML như id, class, src, href, alt...

Ví dụ:

<img src="logo.png" alt="Logo">

Trong đó:

  • src là một Attribute.
  • alt cũng là một Attribute.

Quan hệ giữa các Node

DOM được tổ chức theo cấu trúc cây nên giữa các Node luôn tồn tại mối quan hệ cha, con và anh em.

Ví dụ:

<div id="container">
    <h2>Tiêu đề</h2>
    <p>Đoạn văn</p>
</div>

Trong ví dụ trên:

  • div là phần tử cha.
  • h2 và p là phần tử con.
  • h2 và p là hai phần tử cùng cấp.

JavaScript cung cấp nhiều thuộc tính để di chuyển giữa các Node.

Thuộc tính Ý nghĩa
parentElement Lấy phần tử cha.
children Lấy danh sách phần tử con.
firstElementChild Phần tử con đầu tiên.
lastElementChild Phần tử con cuối cùng.
nextElementSibling Phần tử cùng cấp kế tiếp.
previousElementSibling Phần tử cùng cấp phía trước.

Ví dụ:

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

console.log(box.parentElement);
console.log(box.children);
console.log(box.firstElementChild);

Vì sao DOM quan trọng trong JavaScript?

Nếu không có DOM, JavaScript gần như không thể thay đổi nội dung của một trang web sau khi trang đã được tải. Chính DOM đã biến HTML từ một tài liệu tĩnh thành một giao diện có thể tương tác.

Một số ứng dụng phổ biến của DOM gồm:

  • Thay đổi nội dung văn bản.
  • Hiển thị hoặc ẩn thành phần.
  • Thêm và xóa phần tử.
  • Thay đổi CSS.
  • Xử lý sự kiện chuột và bàn phím.
  • Xây dựng menu, slider, popup.
  • Kiểm tra dữ liệu biểu mẫu.
  • Cập nhật giao diện theo dữ liệu từ API.

Đối với lập trình viên Front-end, việc nắm vững DOM là điều gần như bắt buộc trước khi tiếp cận các thư viện và framework như React, Vue hay Angular. Mặc dù các framework hiện đại đã tối ưu việc thao tác DOM, nhưng hiểu rõ DOM gốc sẽ giúp bạn viết mã hiệu quả hơn, dễ gỡ lỗi hơn và hiểu được cách các framework hoạt động phía sau.

Các phương thức tìm kiếm phần tử trong DOM

Để thao tác với một phần tử, trước tiên JavaScript cần tìm được phần tử đó trong cây DOM. Trình duyệt cung cấp nhiều phương thức khác nhau, mỗi phương thức có ưu điểm và trường hợp sử dụng riêng.

Sử dụng getElementById()

Đây là phương thức nhanh và được sử dụng rất phổ biến khi phần tử có thuộc tính id.

Ví dụ:

const title = document.getElementById("title");

console.log(title);

Ưu điểm:

  • Tốc độ tìm kiếm nhanh.
  • Mỗi id chỉ nên xuất hiện một lần trong tài liệu.
  • Phù hợp với các phần tử quan trọng.

Sử dụng getElementsByClassName()

Phương thức này tìm tất cả phần tử có cùng tên class.

Ví dụ:

const items = document.getElementsByClassName("item");

console.log(items);

Kết quả trả về là một HTMLCollection, không phải mảng JavaScript.

Sử dụng getElementsByTagName()

Dùng để lấy tất cả phần tử có cùng tên thẻ HTML.

Ví dụ:

const paragraphs = document.getElementsByTagName("p");

console.log(paragraphs);

Phương thức này thường dùng khi muốn thao tác hàng loạt với các phần tử cùng loại.

Sử dụng querySelector()

Đây là phương thức hiện đại và linh hoạt nhất. Nó hỗ trợ toàn bộ cú pháp CSS Selector.

Ví dụ:

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

const menu = document.querySelector("#menu");

const image = document.querySelector("img");

Nếu có nhiều phần tử phù hợp, querySelector() chỉ trả về phần tử đầu tiên.

Sử dụng querySelectorAll()

Nếu muốn lấy toàn bộ các phần tử phù hợp với CSS Selector, hãy sử dụng querySelectorAll().

Ví dụ:

const items = document.querySelectorAll(".item");

console.log(items);

Kết quả trả về là một NodeList.

HTMLCollection và NodeList khác nhau như thế nào?

Nhiều người mới học JavaScript thường nhầm lẫn giữa HTMLCollection và NodeList. Mặc dù đều chứa danh sách phần tử nhưng cách hoạt động lại khác nhau.

Đặc điểm HTMLCollection NodeList
Được cập nhật tự động Không (đa số trường hợp)
Có thể dùng forEach() Không
Hỗ trợ CSS Selector Không
Thường được trả về từ getElements... querySelectorAll()

Ví dụ chuyển HTMLCollection thành mảng:

const items = document.getElementsByClassName("item");

const arrayItems = Array.from(items);

Đọc và thay đổi nội dung phần tử

Sau khi lấy được phần tử, thao tác phổ biến nhất là đọc hoặc thay đổi nội dung hiển thị.

textContent

textContent chỉ thao tác với văn bản và không phân tích HTML.

Ví dụ:

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

console.log(title.textContent);

title.textContent = "Web Mới";

Đây là lựa chọn an toàn khi chỉ cần thay đổi văn bản.

innerHTML

innerHTML cho phép đọc và ghi cả nội dung HTML bên trong phần tử.

Ví dụ:

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

box.innerHTML = "<strong>Xin chào</strong>";

Kết quả hiển thị sẽ là chữ "Xin chào" in đậm.

Cần cẩn thận khi sử dụng innerHTML với dữ liệu do người dùng nhập vì có thể phát sinh lỗ hổng bảo mật XSS.

innerText

innerText chỉ lấy phần văn bản đang hiển thị trên giao diện.

Khác với textContent, thuộc tính này chịu ảnh hưởng bởi CSS như display hoặc visibility.

const text = document.querySelector(".content").innerText;

Đọc và thay đổi thuộc tính HTML

DOM cho phép truy cập hầu hết thuộc tính của phần tử HTML.

Ví dụ:

const image = document.querySelector("img");

console.log(image.src);

console.log(image.alt);

getAttribute()

Dùng để lấy giá trị của một thuộc tính.

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

console.log(link.getAttribute("href"));

setAttribute()

Dùng để thêm hoặc cập nhật thuộc tính.

const image = document.querySelector("img");

image.setAttribute("alt", "Logo Web Mới");

removeAttribute()

Xóa một thuộc tính khỏi phần tử.

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

input.removeAttribute("disabled");

hasAttribute()

Kiểm tra xem phần tử có chứa thuộc tính hay không.

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

console.log(input.hasAttribute("required"));

Làm việc với class của phần tử

JavaScript hiện đại sử dụng classList để thao tác với class một cách trực quan hơn.

Thêm class

element.classList.add("active");

Xóa class

element.classList.remove("active");

Bật hoặc tắt class

element.classList.toggle("active");

Nếu class chưa tồn tại thì sẽ được thêm vào. Nếu đã tồn tại thì sẽ bị xóa.

Kiểm tra class

element.classList.contains("active");

Phương thức này trả về giá trị true hoặc false.

Thay thế class

element.classList.replace("old", "new");

Thay đổi CSS bằng JavaScript

Có hai cách phổ biến để thay đổi giao diện bằng DOM.

Thay đổi trực tiếp thuộc tính style

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

box.style.backgroundColor = "orange";

box.style.color = "white";

box.style.padding = "20px";

Cách này phù hợp với các thay đổi nhỏ.

Thêm hoặc xóa class CSS

Trong các dự án thực tế, nên thay đổi class thay vì viết nhiều thuộc tính style.

box.classList.add("show");

Cách này giúp mã JavaScript gọn hơn và dễ bảo trì hơn.

Tạo phần tử mới trong DOM

Một trong những sức mạnh lớn nhất của DOM là khả năng tạo mới các phần tử HTML ngay khi trang đang hoạt động. Điều này giúp xây dựng danh sách sản phẩm, bình luận, bảng dữ liệu hoặc giao diện động mà không cần tải lại trang.

Sử dụng createElement()

Phương thức createElement() tạo ra một phần tử mới nhưng chưa hiển thị trên trang. Muốn phần tử xuất hiện, bạn cần thêm nó vào cây DOM.

Ví dụ:

const div = document.createElement("div");

div.textContent = "Đây là phần tử mới";

Thêm phần tử bằng appendChild()

appendChild() thêm phần tử mới vào cuối danh sách phần tử con.

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

const item = document.createElement("li");

item.textContent = "JavaScript DOM";

list.appendChild(item);

Sử dụng append()

append() là phương thức hiện đại hơn appendChild(). Nó có thể thêm nhiều Node hoặc chuỗi văn bản trong cùng một lần gọi.

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

const title = document.createElement("h3");

title.textContent = "Web Mới";

box.append(title);

Thêm phần tử vào đầu bằng prepend()

Nếu muốn phần tử xuất hiện ở vị trí đầu tiên, prepend() là lựa chọn phù hợp.

const item = document.createElement("li");

item.textContent = "Phần tử đầu tiên";

list.prepend(item);

Chèn phần tử tại vị trí mong muốn

Không phải lúc nào phần tử mới cũng được thêm vào đầu hoặc cuối. DOM cung cấp nhiều phương thức giúp chèn đúng vị trí.

insertBefore()

insertBefore() chèn phần tử mới trước một phần tử đã tồn tại.

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

const newItem = document.createElement("li");

newItem.textContent = "Mục mới";

const firstItem = parent.firstElementChild;

parent.insertBefore(newItem, firstItem);

insertAdjacentHTML()

Đây là phương thức rất hữu ích khi cần chèn trực tiếp một đoạn HTML.

Vị trí Ý nghĩa
beforebegin Trước phần tử hiện tại.
afterbegin Bên trong, ngay đầu phần tử.
beforeend Bên trong, cuối phần tử.
afterend Sau phần tử hiện tại.

Ví dụ:

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

box.insertAdjacentHTML(
    "beforeend",
    "<p>Nội dung mới</p>"
);

So với innerHTML, insertAdjacentHTML() thường có hiệu suất tốt hơn vì không phải tạo lại toàn bộ nội dung bên trong phần tử.

Xóa phần tử khỏi DOM

Khi không còn cần thiết, bạn có thể loại bỏ phần tử để giải phóng bộ nhớ và cập nhật giao diện.

remove()

Đây là cách đơn giản nhất.

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

box.remove();

removeChild()

Trong một số trường hợp cần tương thích với trình duyệt cũ, removeChild() vẫn được sử dụng.

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

const item = parent.firstElementChild;

parent.removeChild(item);

Thay thế phần tử

replaceChild() cho phép thay thế một phần tử cũ bằng phần tử mới.

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

const newTitle = document.createElement("h2");

newTitle.textContent = "DOM JavaScript";

const oldTitle = parent.querySelector("h3");

parent.replaceChild(newTitle, oldTitle);

Sao chép phần tử

cloneNode() giúp tạo một bản sao của phần tử.

Sao chép không bao gồm phần tử con

const copy = element.cloneNode(false);

Sao chép toàn bộ cây DOM bên trong

const copy = element.cloneNode(true);

Tham số true nghĩa là sao chép cả phần tử con, văn bản và toàn bộ cấu trúc bên trong.

Duyệt các phần tử trong DOM

Khi làm việc với danh sách dữ liệu, bạn thường cần lặp qua nhiều phần tử.

Dùng for...of

const items = document.querySelectorAll(".item");

for (const item of items) {
    console.log(item.textContent);
}

Dùng forEach()

const items = document.querySelectorAll(".item");

items.forEach(function(item) {
    console.log(item.textContent);
});

Dùng vòng lặp for

const items = document.querySelectorAll(".item");

for (let i = 0; i < items.length; i++) {
    console.log(items[i]);
}

Trong các dự án hiện đại, forEach() và for...of thường được ưu tiên vì dễ đọc và ngắn gọn.

Làm việc với Form bằng DOM

DOM hỗ trợ truy cập toàn bộ dữ liệu người dùng nhập vào biểu mẫu.

Lấy giá trị của ô nhập liệu

const username = document.querySelector("#username");

console.log(username.value);

Đặt giá trị cho ô nhập liệu

username.value = "Web Mới";

Đánh dấu ô nhập liệu

username.focus();

Bỏ đánh dấu

username.blur();

Chọn toàn bộ nội dung

username.select();

Lấy kích thước và vị trí phần tử

DOM cung cấp nhiều thuộc tính giúp xác định chiều rộng, chiều cao cũng như vị trí của phần tử trên trang.

Thuộc tính Mô tả
offsetWidth Chiều rộng bao gồm border.
offsetHeight Chiều cao bao gồm border.
clientWidth Chiều rộng không tính border.
clientHeight Chiều cao không tính border.
scrollWidth Chiều rộng toàn bộ nội dung.
scrollHeight Chiều cao toàn bộ nội dung.

Ví dụ:

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

console.log(box.offsetWidth);

console.log(box.offsetHeight);

Lấy tọa độ phần tử

getBoundingClientRect() trả về vị trí của phần tử so với cửa sổ trình duyệt.

const rect = box.getBoundingClientRect();

console.log(rect.top);

console.log(rect.left);

console.log(rect.width);

console.log(rect.height);

Đây là phương thức thường được sử dụng khi xây dựng hiệu ứng cuộn trang, lazy loading, tooltip hoặc kéo thả (Drag and Drop).

Xử lý sự kiện trong DOM

DOM sẽ phát sinh sự kiện mỗi khi người dùng hoặc trình duyệt thực hiện một hành động nào đó như nhấp chuột, nhập dữ liệu, cuộn trang hay thay đổi kích thước cửa sổ. JavaScript có thể lắng nghe các sự kiện này để thực hiện các chức năng tương ứng.

Đăng ký sự kiện bằng addEventListener()

Đây là phương thức được khuyến nghị sử dụng trong JavaScript hiện đại vì có tính linh hoạt cao và cho phép gắn nhiều sự kiện trên cùng một phần tử.

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

button.addEventListener("click", function () {
    console.log("Đã nhấn nút");
});

Khi người dùng nhấp vào nút, hàm xử lý sẽ được thực thi.

Hàm xử lý sự kiện

Hàm xử lý có thể được khai báo riêng để dễ bảo trì và tái sử dụng.

function handleClick() {
    console.log("Xin chào Web Mới");
}

button.addEventListener("click", handleClick);

Gỡ bỏ sự kiện

Trong một số trường hợp, bạn cần loại bỏ sự kiện để tránh thực thi nhiều lần hoặc giảm tiêu tốn tài nguyên.

button.removeEventListener("click", handleClick);

Lưu ý rằng removeEventListener() chỉ hoạt động khi truyền đúng tham chiếu đến hàm đã đăng ký trước đó.

Các sự kiện DOM thường gặp

Sự kiện chuột

Sự kiện Mô tả
click Nhấp chuột.
dblclick Nhấp đúp chuột.
mousedown Nhấn chuột xuống.
mouseup Thả chuột.
mousemove Di chuyển chuột.
mouseenter Di chuyển chuột vào phần tử.
mouseleave Di chuyển chuột ra khỏi phần tử.
contextmenu Nhấp chuột phải.

Ví dụ:

box.addEventListener("mouseenter", function () {
    console.log("Chuột đang ở trong phần tử");
});

Sự kiện bàn phím

Sự kiện Mô tả
keydown Khi nhấn phím.
keyup Khi nhả phím.

Ví dụ:

document.addEventListener("keydown", function (event) {
    console.log(event.key);
});

Thuộc tính key giúp xác định chính xác phím mà người dùng vừa nhấn.

Sự kiện biểu mẫu

Sự kiện Mô tả
submit Gửi biểu mẫu.
change Giá trị thay đổi.
input Nhập dữ liệu.
focus Nhận tiêu điểm.
blur Mất tiêu điểm.

Ví dụ:

const input = document.querySelector("#keyword");

input.addEventListener("input", function () {
    console.log(input.value);
});

Sự kiện input thường được sử dụng để tạo chức năng tìm kiếm theo thời gian thực.

Sự kiện cửa sổ trình duyệt

Sự kiện Mô tả
load Trang tải hoàn tất.
resize Thay đổi kích thước cửa sổ.
scroll Cuộn trang.
window.addEventListener("scroll", function () {
    console.log(window.scrollY);
});

Đối tượng Event

Mỗi khi sự kiện xảy ra, JavaScript sẽ truyền vào hàm xử lý một đối tượng Event chứa rất nhiều thông tin liên quan.

button.addEventListener("click", function (event) {
    console.log(event);
});

Một số thuộc tính thường dùng:

Thuộc tính Ý nghĩa
target Phần tử phát sinh sự kiện.
currentTarget Phần tử đang xử lý sự kiện.
type Tên sự kiện.
timeStamp Thời điểm xảy ra sự kiện.

Ví dụ:

button.addEventListener("click", function (event) {
    console.log(event.target);
});

Ngăn hành động mặc định

Nhiều phần tử HTML có hành động mặc định, chẳng hạn như thẻ a sẽ chuyển trang hoặc form sẽ gửi dữ liệu.

preventDefault() giúp ngăn hành động này.

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

form.addEventListener("submit", function (event) {
    event.preventDefault();

    console.log("Biểu mẫu chưa được gửi");
});

Đây là kỹ thuật rất phổ biến khi kiểm tra dữ liệu biểu mẫu bằng JavaScript.

Ngăn sự kiện lan truyền

Mặc định, sự kiện sẽ lan truyền từ phần tử con lên phần tử cha. Quá trình này gọi là Event Bubbling.

Nếu muốn dừng việc lan truyền, hãy sử dụng stopPropagation().

child.addEventListener("click", function (event) {
    event.stopPropagation();

    console.log("Chỉ xử lý tại phần tử con");
});

Event Bubbling và Event Capturing

DOM hỗ trợ hai cơ chế truyền sự kiện.

Cơ chế Hướng truyền
Capturing Từ phần tử gốc xuống phần tử con.
Bubbling Từ phần tử con lên phần tử cha.

Mặc định, addEventListener() sử dụng Bubbling.

Nếu muốn sử dụng Capturing:

element.addEventListener("click", handleClick, true);

Event Delegation

Thay vì đăng ký sự kiện cho từng phần tử, Event Delegation chỉ cần đăng ký một lần trên phần tử cha. Kỹ thuật này đặc biệt hữu ích khi danh sách phần tử được tạo động.

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

list.addEventListener("click", function (event) {
    if (event.target.matches("li")) {
        console.log(event.target.textContent);
    }
});

Ưu điểm của Event Delegation:

  • Giảm số lượng Event Listener.
  • Tăng hiệu suất.
  • Hỗ trợ tốt với các phần tử được tạo sau khi trang đã tải.
  • Dễ bảo trì khi làm việc với danh sách lớn.

DOM Traversing

DOM Traversing là quá trình di chuyển giữa các phần tử trong cây DOM để tìm đúng đối tượng cần thao tác.

Thuộc tính Chức năng
parentElement Lấy phần tử cha.
children Lấy toàn bộ phần tử con.
firstElementChild Phần tử con đầu tiên.
lastElementChild Phần tử con cuối cùng.
nextElementSibling Phần tử kế tiếp.
previousElementSibling Phần tử phía trước.
closest() Tìm phần tử cha gần nhất theo CSS Selector.

Ví dụ:

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

const form = button.closest("form");

console.log(form);

closest() rất hữu ích khi xử lý Event Delegation hoặc làm việc với các giao diện có cấu trúc HTML phức tạp.

DOM Collections

Khi làm việc với DOM, bạn sẽ thường xuyên nhận được một tập hợp phần tử thay vì chỉ một phần tử duy nhất. Việc hiểu rõ từng loại Collection sẽ giúp tránh nhiều lỗi trong quá trình lập trình.

HTMLCollection

HTMLCollection là danh sách các phần tử HTML được trả về từ một số phương thức như getElementsByClassName() hoặc getElementsByTagName().

Đặc điểm của HTMLCollection:

  • Là một Collection, không phải mảng.
  • Được cập nhật tự động khi DOM thay đổi (Live Collection).
  • Không hỗ trợ trực tiếp các phương thức của Array.

Ví dụ:

const items = document.getElementsByClassName("item");

console.log(items.length);

Nếu sau đó bạn thêm một phần tử có class item, HTMLCollection sẽ tự động tăng số lượng phần tử mà không cần gọi lại phương thức.

NodeList

NodeList thường được trả về bởi querySelectorAll().

Đặc điểm:

  • Không tự cập nhật khi DOM thay đổi trong đa số trường hợp.
  • Hỗ trợ forEach().
  • Dễ thao tác hơn HTMLCollection.

Ví dụ:

const items = document.querySelectorAll(".item");

items.forEach(function(item) {
    console.log(item.textContent);
});

Chuyển Collection thành Array

Khi cần sử dụng các phương thức như map(), filter() hoặc reduce(), bạn nên chuyển Collection thành mảng.

Sử dụng Array.from():

const items = document.getElementsByClassName("item");

const arrayItems = Array.from(items);

Hoặc sử dụng Spread Operator:

const items = document.querySelectorAll(".item");

const arrayItems = [...items];

Làm việc với thuộc tính data-*

HTML5 cho phép lưu trữ dữ liệu tùy chỉnh thông qua các thuộc tính data-*. Đây là cách rất phổ biến để truyền dữ liệu từ HTML sang JavaScript.

Ví dụ HTML:

<button
    data-id="15"
    data-category="javascript">
    Xem chi tiết
</button>

Truy cập bằng dataset:

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

console.log(button.dataset.id);

console.log(button.dataset.category);

Nếu thuộc tính có dấu gạch ngang, JavaScript sẽ tự chuyển sang camelCase.

Ví dụ:

<div data-user-name="admin"></div>

Truy cập:

element.dataset.userName;

Thuộc tính và Property trong DOM có gì khác nhau?

Nhiều lập trình viên mới thường nhầm lẫn giữa Attribute và Property. Hai khái niệm này liên quan chặt chẽ nhưng không hoàn toàn giống nhau.

Attribute Property
Là giá trị trong HTML. Là giá trị của đối tượng JavaScript.
Đọc bằng getAttribute(). Đọc trực tiếp qua đối tượng.
Không phải lúc nào cũng thay đổi theo trạng thái hiện tại. Luôn phản ánh trạng thái hiện tại.

Ví dụ:

<input value="JavaScript">
const input = document.querySelector("input");

console.log(input.getAttribute("value"));

console.log(input.value);

Nếu người dùng thay đổi nội dung ô nhập liệu:

  • getAttribute("value") vẫn trả về giá trị ban đầu.
  • input.value trả về giá trị mới mà người dùng vừa nhập.

Document Fragment

Khi cần thêm hàng trăm hoặc hàng nghìn phần tử vào DOM, việc thêm từng phần tử trực tiếp có thể làm giảm hiệu suất do trình duyệt phải cập nhật giao diện liên tục.

DocumentFragment giúp giải quyết vấn đề này bằng cách tạo một vùng nhớ tạm để xây dựng toàn bộ cấu trúc trước khi chèn vào DOM.

Ví dụ:

const fragment = document.createDocumentFragment();

for (let i = 1; i <= 1000; i++) {
    const li = document.createElement("li");

    li.textContent = "Item " + i;

    fragment.appendChild(li);
}

document.querySelector("ul").appendChild(fragment);

Ưu điểm:

  • Giảm số lần Reflow.
  • Tăng tốc độ hiển thị.
  • Đặc biệt hiệu quả với danh sách lớn.

DOM Reflow và Repaint

Khi DOM hoặc CSS thay đổi, trình duyệt phải tính toán và cập nhật giao diện. Quá trình này thường gồm hai bước.

Reflow

Reflow xảy ra khi bố cục của trang thay đổi.

Ví dụ:

  • Thêm phần tử mới.
  • Xóa phần tử.
  • Thay đổi chiều rộng.
  • Thay đổi chiều cao.
  • Đổi font chữ.

Đây là thao tác khá tốn tài nguyên.

Repaint

Repaint xảy ra khi chỉ thay đổi giao diện mà không ảnh hưởng đến bố cục.

Ví dụ:

  • Đổi màu chữ.
  • Đổi màu nền.
  • Đổi border-color.

Repaint thường nhẹ hơn Reflow.

Cách tối ưu thao tác DOM

DOM là một trong những thành phần có chi phí xử lý cao trong trình duyệt. Việc tối ưu sẽ giúp website hoạt động mượt hơn.

Hạn chế truy cập DOM nhiều lần

Không nên:

document.querySelector(".box").style.color = "red";

document.querySelector(".box").style.background = "yellow";

document.querySelector(".box").style.padding = "20px";

Nên:

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

box.style.color = "red";
box.style.background = "yellow";
box.style.padding = "20px";

Ưu tiên thay đổi class thay vì style

Không nên sửa từng thuộc tính CSS bằng JavaScript nếu có thể giải quyết bằng class.

box.classList.add("active");

Cách này giúp mã nguồn rõ ràng hơn và CSS dễ quản lý hơn.

Gom nhiều thay đổi lại một lần

Thay vì cập nhật DOM liên tục, hãy thực hiện nhiều thay đổi rồi mới chèn vào tài liệu.

DocumentFragment là một trong những giải pháp phổ biến cho mục đích này.

Không lặp querySelector() nhiều lần

Nên lưu kết quả vào biến để tái sử dụng.

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

Sử dụng Event Delegation

Khi có hàng trăm phần tử giống nhau, không nên gắn Event Listener cho từng phần tử.

Thay vào đó hãy đăng ký sự kiện trên phần tử cha để giảm số lượng Event Listener.

Những lỗi phổ biến khi làm việc với DOM

Thao tác khi phần tử chưa tồn tại

Ví dụ:

document.querySelector(".box").textContent = "Hello";

Nếu phần tử chưa được tạo, chương trình sẽ báo lỗi.

Nên kiểm tra:

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

if (box) {
    box.textContent = "Hello";
}

Quên đợi DOM tải xong

Nếu JavaScript chạy trước khi HTML được tạo, việc tìm phần tử sẽ thất bại.

Có thể lắng nghe sự kiện DOMContentLoaded:

document.addEventListener("DOMContentLoaded", function () {
    console.log("DOM đã sẵn sàng");
});

Lạm dụng innerHTML

innerHTML rất tiện lợi nhưng nếu sử dụng với dữ liệu không được kiểm soát có thể gây ra lỗ hổng Cross-site Scripting (XSS).

Trong trường hợp chỉ cần hiển thị văn bản, nên ưu tiên sử dụng textContent để đảm bảo an toàn.

Shadow DOM là gì?

Trong quá trình phát triển các ứng dụng web hiện đại, bạn có thể bắt gặp khái niệm Shadow DOM. Đây là một tính năng của nền tảng Web Components giúp tạo ra một cây DOM riêng biệt, tách biệt với DOM chính của trang.

Nhờ đó, HTML, CSS và JavaScript bên trong Shadow DOM sẽ không bị ảnh hưởng bởi các thành phần bên ngoài và cũng không tác động đến giao diện của các phần tử khác.

Ví dụ tạo Shadow DOM:

const element = document.querySelector(".card");

const shadow = element.attachShadow({
    mode: "open"
});

shadow.innerHTML = "<h3>Web Mới</h3>";

Có hai chế độ khi tạo Shadow DOM:

Chế độ Ý nghĩa
open Có thể truy cập thông qua thuộc tính shadowRoot.
closed Không thể truy cập trực tiếp từ bên ngoài.

Shadow DOM thường được sử dụng khi xây dựng các Web Components hoặc thư viện giao diện nhằm tránh xung đột CSS và JavaScript.

DOM và Virtual DOM khác nhau như thế nào?

Khi tìm hiểu React hoặc các framework hiện đại, bạn sẽ thường nghe đến Virtual DOM. Đây không phải là DOM của trình duyệt mà là một bản sao được tạo trong bộ nhớ.

DOM Virtual DOM
Là cây DOM thật của trình duyệt. Là mô hình DOM nằm trong bộ nhớ.
Thay đổi trực tiếp giao diện. So sánh sự khác biệt trước khi cập nhật DOM thật.
Mỗi lần chỉnh sửa đều có thể gây Reflow hoặc Repaint. Giảm số lần cập nhật DOM.
Là tiêu chuẩn của trình duyệt. Là kỹ thuật được các framework áp dụng.

Cần lưu ý rằng Virtual DOM không thay thế DOM thật. Cuối cùng, mọi thay đổi vẫn phải được đồng bộ xuống DOM của trình duyệt để hiển thị cho người dùng.

Một số DOM API hiện đại nên biết

Ngoài những phương thức phổ biến, JavaScript còn cung cấp nhiều API hữu ích giúp thao tác DOM thuận tiện hơn.

matches()

Kiểm tra xem phần tử có khớp với CSS Selector hay không.

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

console.log(button.matches(".btn"));

closest()

Tìm phần tử cha gần nhất phù hợp với CSS Selector.

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

const form = input.closest("form");

contains()

Kiểm tra một phần tử có chứa phần tử khác hay không.

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

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

console.log(box.contains(button));

replaceChildren()

Xóa toàn bộ phần tử con và thay thế bằng nội dung mới.

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

const title = document.createElement("h2");

title.textContent = "DOM JavaScript";

box.replaceChildren(title);

before() và after()

Hai phương thức này giúp chèn phần tử ngay trước hoặc ngay sau một phần tử khác.

const title = document.createElement("h2");

title.textContent = "Tiêu đề";

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

box.before(title);

Khi nào nên thao tác trực tiếp với DOM?

Mặc dù các framework như React, Vue hay Angular đã hỗ trợ quản lý giao diện rất hiệu quả, việc thao tác trực tiếp với DOM vẫn rất cần thiết trong nhiều trường hợp.

  • Xây dựng website bằng HTML, CSS và JavaScript thuần.
  • Viết plugin hoặc thư viện JavaScript.
  • Tạo hiệu ứng giao diện.
  • Kiểm tra dữ liệu biểu mẫu.
  • Thực hiện Drag and Drop.
  • Xây dựng Slider, Carousel, Menu, Modal.
  • Phát triển các tiện ích mở rộng cho trình duyệt.

Ngay cả khi sử dụng framework hiện đại, hiểu rõ DOM vẫn giúp bạn dễ dàng phân tích lỗi, tối ưu hiệu suất và hiểu được cách framework hoạt động phía sau.

Các câu hỏi thường gặp về DOM trong JavaScript

DOM có phải là JavaScript không?

Không. DOM là một API do trình duyệt cung cấp để biểu diễn tài liệu HTML dưới dạng cây đối tượng. JavaScript chỉ là ngôn ngữ sử dụng API này để đọc và thay đổi nội dung của trang web.

DOM và HTML có giống nhau không?

Không hoàn toàn. HTML là mã nguồn ban đầu của trang web, còn DOM là cấu trúc đối tượng được trình duyệt tạo ra sau khi phân tích HTML.

querySelector() hay getElementById() nên dùng phương thức nào?

Nếu cần tìm một phần tử theo id và muốn đạt tốc độ tối ưu, getElementById() là lựa chọn phù hợp. Tuy nhiên, trong hầu hết các dự án hiện đại, querySelector() được sử dụng nhiều hơn vì hỗ trợ đầy đủ CSS Selector và giúp mã nguồn thống nhất.

textContent và innerHTML nên dùng khi nào?

Nếu chỉ cần hiển thị văn bản, hãy ưu tiên textContent vì an toàn và có hiệu suất tốt. Chỉ sử dụng innerHTML khi thực sự cần chèn hoặc thay đổi cấu trúc HTML.

Vì sao thao tác DOM nhiều lần làm website chậm?

Mỗi lần DOM thay đổi, trình duyệt có thể phải tính toán lại bố cục (Reflow) hoặc vẽ lại giao diện (Repaint). Nếu việc cập nhật diễn ra liên tục, hiệu suất của trang sẽ giảm, đặc biệt trên các thiết bị có cấu hình thấp.

Có cần học DOM trước khi học React hoặc Vue không?

Nên. React, Vue và nhiều framework khác đều xây dựng dựa trên nguyên lý hoạt động của DOM. Khi hiểu rõ DOM, bạn sẽ dễ tiếp cận Virtual DOM, cơ chế cập nhật giao diện và xử lý sự kiện của các framework này.

Tổng kết

DOM là nền tảng giúp JavaScript có thể tương tác với trang web sau khi được tải trên trình duyệt. Từ việc tìm kiếm phần tử, thay đổi nội dung, cập nhật CSS, xử lý sự kiện cho đến tạo hoặc xóa các thành phần HTML, mọi thao tác đều dựa trên hệ thống API mà DOM cung cấp.

Việc nắm vững DOM không chỉ giúp bạn xây dựng các website có khả năng tương tác cao mà còn tạo nền tảng vững chắc để học các thư viện và framework phổ biến như React, Vue hay Angular. Thay vì ghi nhớ từng phương thức một cách máy móc, hãy tập trung hiểu cách cây DOM được tổ chức, mối quan hệ giữa các Node và nguyên lý cập nhật giao diện của trình duyệt.

Khi đã hiểu bản chất của DOM, bạn sẽ viết JavaScript dễ dàng hơn, tối ưu hiệu suất tốt hơn và chủ động xử lý hầu hết các tình huống thường gặp trong quá trình phát triển website.

  • 0 Bình luận
Trần Thị Vân | Content Creator Web Mới
Trần Thị Vân
Trần Thị Vân là Content Creator tại Web Mới, phụ trách nghiên cứu, biên soạn nội dung và chia sẻ kiến thức về website, SEO, lập trình cùng các xu hướng công nghệ
Chia sẻ nội dung đánh giá của bạn về Tổng hợp DOM trong JavaScript
Email, Điện thoại của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *
Đánh giá của bạn
Tên *
Email
Số điện thoại *
Bình luận, Hỏi đáp
Đăng ký tư vấn thiết kế website
Tìm hiểu 1 năm không bằng lắng nghe 1 câu tư vấn