Buổi 12: JavaScript cho Website (1): window, document, sự kiện, DOM
Bạn đã biết viết JavaScript "thuần": biến, hàm, mảng, đối tượng. Nhưng một trang web thật còn phải phản ứng — bấm nút "Thêm vào giỏ" thì số lượng tăng lên, gõ vào ô tìm kiếm thì danh sách lọc theo, bấm vào một dòng sản phẩm thì hiện chi tiết. Muốn vậy, JavaScript phải "nhìn thấy" và "chạm vào" được trang web đang hiển thị. Cầu nối đó chính là đối tượng cửa sổ (window), đối tượng tài liệu (document) và mô hình đối tượng tài liệu (DOM). Buổi này bạn sẽ học cách tìm một phần tử trong trang, đổi nội dung và định dạng của nó, lắng nghe thao tác của người dùng, rồi tạo hoặc xóa phần tử ngay trong lúc trang đang chạy.
Mục tiêu buổi học
- Sau buổi này bạn có thể mô tả vai trò của đối tượng window và các đối tượng con (
document,location,history,navigator,screen,localStorage,sessionStorage), dùng hộp thoạialert(),confirm(),prompt()và bộ hẹn giờsetTimeout(),setInterval(). - Sau buổi này bạn có thể tìm phần tử HTML bằng
getElementById(),getElementsByTagName(),getElementsByClassName(),querySelector(),querySelectorAll()và phân biệtHTMLCollectionvớiNodeList. - Sau buổi này bạn có thể thay đổi nội dung, thuộc tính và định dạng (
style) của phần tử, và biết khi nào cần dùnggetComputedStyle(). - Sau buổi này bạn có thể gắn, gỡ trình xử lý sự kiện bằng
addEventListener()/removeEventListener(), giải thích ba pha capturing – target – bubbling và dùngpreventDefault(),stopPropagation(). - Sau buổi này bạn có thể duyệt cây DOM bằng các quan hệ cha – con – anh em và tạo mới (
createElement(),appendChild(),insertBefore()), xóa (remove(),removeChild()) phần tử.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1. Đối tượng cửa sổ | window, hộp thoại, hẹn giờ, screen, location, history, navigator, cookie, localStorage, sessionStorage | 35 phút |
| 2. Đối tượng tài liệu | Tìm phần tử HTML, thay đổi thuộc tính, style |
40 phút |
| 3. Xử lý sự kiện | Gắn sự kiện, lan truyền sự kiện, đối tượng sự kiện | 45 phút |
| 4. Nút (Node) | Quan hệ giữa các nút, thuộc tính của nút, tạo và xóa phần tử | 40 phút |
| Thực hành & ôn tập | Thực hành theo giáo trình, câu hỏi ôn tập, bài tập trên Codelab | 20 phút |
Khoảng một nửa thời gian của buổi là thực hành: sau mỗi phần lý thuyết bạn gõ và chạy thử mã ngay trong bài, rồi làm nhóm bài tập tương ứng trên Codelab.
1. Đối tượng cửa sổ
Giáo trình: Chương 6 — mục "Đối tượng cửa sổ" (Khái niệm đối tượng cửa sổ; Các phương thức quan trọng; Các thuộc tính quan trọng).
1.1. Khái niệm đối tượng cửa sổ
Khi lập trình JavaScript trong trình duyệt, cửa sổ (window) là một đối tượng có thể truy cập bất kỳ lúc nào. Đối tượng window cung cấp các đối tượng và phương thức phục vụ việc lập trình cho Website. Bạn có thể in thử bằng console.log(window) để xem — bạn sẽ thấy một danh sách rất dài các thuộc tính và phương thức.
- 1
window.historyLịch sử duyệt: lùi, tiến giữa các trang đã xemhistory.back() → history.length → 5 - 2
window.locationĐịa chỉ trang hiện tại; gán giá trị mới để chuyển tranglocation.href → "https://fit.neu.edu.vn/index.html?id=5#top" - 3
window.documentTài liệu HTML đang hiển thị: cây DOM của trangdocument.title → getElementById("demo") - 4
window.navigatorThông tin trình duyệt: loại, ngôn ngữ, trạng thái mạngnavigator.userAgent → "Mozilla/5.0 … Chrome" - 5
window.screenThông tin màn hình của máy người dùngscreen.width → 1920 × 1080
Tiền tố window. có thể bỏ khi gọi.
alert(outerWidth + ":" + outerHeight) // "1440:900"
window là đối tượng toàn cục của trình duyệt: document, location, history, navigator, screen đều là thuộc tính của nó, nên có thể viết alert() thay cho window.alert().
Sơ đồ trên cho thấy window là đối tượng "gốc", bên trong chứa document, history, location, navigator, screen... Hãy đọc nó như một cái cây: mọi thứ bạn dùng trong trình duyệt đều "treo" dưới window.
Đối tượng window có rất nhiều phương thức hữu ích, ví dụ:
window.alert(): hiển thị một thông báo.window.open(): mở một cửa sổ mới.window.close(): đóng cửa sổ hiện tại.window.moveTo(): di chuyển cửa sổ đến một vị trí xác định.window.resizeTo(): thay đổi kích thước cửa sổ.
Bên cạnh đó, window chứa nhiều thuộc tính bản thân cũng là các đối tượng, ví dụ:
| Thuộc tính | Ý nghĩa |
|---|---|
caches |
Đối tượng chứa thông tin về bộ nhớ đệm |
document |
Đối tượng chứa thông tin về tài liệu HTML |
history |
Đối tượng chứa thông tin về lịch sử |
location |
Đối tượng chứa thông tin về địa chỉ Web |
navigator |
Đối tượng trình duyệt |
screen |
Đối tượng chứa thông tin về màn hình |
Điều đặc biệt là để truy cập thuộc tính và phương thức của window, bạn không cần ghi window. đằng trước. Ví dụ, thay vì viết window.alert(window.outerWidth + ":" + window.outerHeight) bạn chỉ cần viết alert(outerWidth + ":" + outerHeight). Ở đây alert() là phương thức của window còn outerWidth, outerHeight là chiều rộng và chiều cao của cửa sổ trình duyệt.
Hãy tự kiểm chứng rằng alert và window.alert là một, document và window.document là một:
console.log(window.alert === alert);
console.log(window.document === document);
console.log(typeof window.outerWidth, typeof outerWidth);
console.log(typeof window.setTimeout, typeof window.localStorage);Lưu ý: Mọi biến toàn cục (
var) và hàm khai báo ở phạm vi ngoài cùng đều trở thành thuộc tính củawindow. Vì vậywindowcòn được gọi là đối tượng toàn cục của trình duyệt (ở buổi 10 bạn đã gặp phạm vi toàn cục).
1.2. Các phương thức quan trọng của đối tượng cửa sổ
Hộp thoại alert(), confirm(), prompt()
Hộp thoại là công cụ đơn giản nhất để Website giao tiếp với người dùng. Phương thức alert() hiển thị hộp thoại thông báo (hoặc cảnh báo). Khi nó bật lên, người dùng bấm OK hoặc Close (tùy trình duyệt) để tiếp tục. Giao diện hộp thoại khác nhau giữa các trình duyệt và nhà phát triển thường không thể thay đổi. Để xuống dòng trong hộp thoại, dùng \n (dấu gạch chéo ngược theo sau là chữ n).
Phương thức confirm() hiển thị hộp thoại xác nhận để người dùng xác minh hoặc chấp nhận một điều gì đó. Người dùng phải bấm "OK" hoặc "Cancel": bấm OK thì confirm() trả về true, bấm Cancel thì trả về false. Nhờ vậy bạn biết người dùng chọn phương án nào.
Phương thức prompt() hiển thị hộp thoại nhập để lấy giá trị người dùng gõ vào. Bấm "OK" thì trả về giá trị đầu vào (chuỗi), bấm "Cancel" thì trả về null. Tham số thứ hai là giá trị gợi ý mặc định.
alert("Xin chào\nChúc một ngày tốt lành");
let luachon = confirm("Bạn có chắc không");
console.log("confirm trả về:", luachon);
let ten = prompt("Tên", "Tên của bạn");
console.log("Xin chào " + ten);Trong khung chạy thử của bài này, confirm() luôn trả true và prompt() trả về giá trị đã cài sẵn (ở đây là Lan); trên trình duyệt thật người dùng sẽ tự bấm và gõ.
Mẹo: Hộp thoại chặn mọi thao tác trên trang cho tới khi người dùng đóng nó. Hãy dùng hạn chế: chỉ để xác nhận hành động nguy hiểm (như xóa) hoặc khi học và gỡ lỗi nhanh. Giao diện chuyên nghiệp thường tự dựng hộp thoại bằng HTML/CSS.
Hẹn giờ: setTimeout(), setInterval(), clearTimeout(), clearInterval()
JavaScript cho phép thực thi mã theo các khoảng thời gian xác định.
setTimeout(hàm, ms)thực thi một hàm một lần sau khi chờ một khoảng thời gian (tính bằng mili giây).setInterval(hàm, ms)giốngsetTimeout()nhưng lặp lại việc gọi hàm theo chu kỳ cho tới khi bị dừng.clearTimeout(id)hủy bộ hẹn giờ dosetTimeout()thiết lập;clearInterval(id)hủy bộ hẹn giờ dosetInterval()thiết lập. Cả hai nhận tham số là giá trị màsetTimeout()hoặcsetInterval()đã trả về.
Ví dụ trong giáo trình in chữ "Hiển thị thông tin" sau 1000 mili giây; ở dưới là bản rút ngắn thời gian để bạn xem ngay, kèm bộ đếm lặp 3 lần rồi dừng:
const thoigian = setTimeout(function () {
console.log("Hiển thị thông tin (sau 300 ms)");
}, 300);
let dem = 0;
const lap = setInterval(function () {
dem++;
console.log("Lần lặp thứ " + dem);
if (dem === 3) {
clearInterval(lap);
console.log("Đã dừng setInterval");
}
}, 50);
const huy = setTimeout(function () { console.log("Dòng này không bao giờ in"); }, 80);
clearTimeout(huy);
console.log("Dòng này in ngay, trước các dòng hẹn giờ");Hãy chú ý thứ tự in: dòng cuối cùng của mã lại in đầu tiên, vì các hàm hẹn giờ chỉ chạy sau khi đoạn mã chính đã xong. Đây là bước làm quen với tính chất bất đồng bộ mà buổi 13 sẽ nói kỹ hơn.
open(), close(), moveTo(), moveBy(), resizeTo(), resizeBy()
Phương thức open() và close() dùng để mở một cửa sổ mới hoặc đóng cửa sổ hiện tại. Ví dụ mở cửa sổ hiển thị tệp quangcao.html, 3 giây sau thì đóng lại:
var quangcao = open("quangcao.html", "", "width=600, height=400")
setTimeout(function () {
quangcao.close()
}, 3000);Cần chú ý: trình duyệt phải cho phép mở cửa sổ bật lên (popup) thì lệnh open() mới có tác dụng; mặc định nhiều trình duyệt chặn popup nếu không phải do người dùng bấm. Với cửa sổ vừa mở, bạn có thể di chuyển và đổi kích thước:
var quangcao = open("quangcao.html", "", "width=600, height=400")
quangcao.moveTo(100, 100); // Di chuyển đến vị trí (100,100)
quangcao.moveBy(50, 50); // Di chuyển đến vị trí mới cách vị trí ban đầu 50 sang phải và 50 xuống dưới
quangcao.resizeTo(300, 300); // Thay đổi kích thước thành 300px x 300px
quangcao.resizeBy(100, 100); // Tăng thêm 100px mỗi chiềuQuy tắc nhớ: ...To(x, y) là đặt đến vị trí/kích thước tuyệt đối; ...By(x, y) là thay đổi tương đối so với hiện tại.
Cảnh báo: Các ví dụ trên mở cửa sổ mới nên không có nút chạy thử trong bài (khung chạy thử bị cách ly, không điều hướng và không mở cửa sổ). Bạn hãy thử trong tệp HTML trên máy mình với trình duyệt đã cho phép popup. Trình duyệt hiện đại còn chỉ cho
moveTo()/resizeTo()tác động lên cửa sổ doopen()tạo ra.
scrollTo(), scrollBy() và print()
Phương thức scrollBy() và scrollTo() dùng để cuộn cửa sổ:
scrollTo(0, 100); // Cuộn theo trục tung (cuộn dọc) đến vị trí 100
scrollBy(0, 50); // Cuộn xuống thêm 50 so với vị trí hiện tạiPhương thức print() dùng để in nội dung của cửa sổ, có tác dụng tương tự khi người dùng chọn Print... trong trình duyệt.
Bảng tóm tắt các phương thức:
| Phương thức | Tác dụng |
|---|---|
alert(m) |
Thông báo, không trả giá trị |
confirm(m) |
Xác nhận; true (OK) / false (Cancel) |
prompt(m, mặc định) |
Hộp nhập; chuỗi (OK) / null (Cancel) |
setTimeout / clearTimeout |
Chạy một lần sau X ms / hủy |
setInterval / clearInterval |
Chạy lặp mỗi X ms / hủy |
open / close |
Mở / đóng cửa sổ |
moveTo / moveBy |
Di chuyển tuyệt đối / tương đối |
resizeTo / resizeBy |
Đổi kích thước tuyệt đối / tương đối |
scrollTo / scrollBy |
Cuộn tuyệt đối / tương đối |
print() |
In trang |
1.3. Các thuộc tính quan trọng của đối tượng cửa sổ
Từ window bạn truy cập được nhiều thuộc tính, bản thân chúng cũng là những đối tượng quan trọng.
Đối tượng màn hình (screen)
Đối tượng màn hình (screen) chứa thông tin về màn hình của người dùng:
| Thuộc tính | Ý nghĩa |
|---|---|
width |
chiều rộng màn hình |
height |
chiều cao màn hình |
availWidth |
chiều rộng khả dụng của màn hình |
availHeight |
chiều cao khả dụng của màn hình |
colorDepth |
độ sâu màu màn hình |
isExtended |
có màn hình ngoài hay không |
orientation |
màn hình xoay dọc hay xoay ngang |
width/height là kích thước cả màn hình, còn availWidth/availHeight là kích thước "khả dụng" — phần có thể dùng cho trình duyệt (đã trừ thanh tác vụ). Ví dụ in kích thước khả dụng:
alert("Chiều rộng:" + screen.availWidth + ", Chiều cao: " + screen.availHeight)Vì kết quả phụ thuộc vào máy của bạn nên ví dụ này chỉ minh họa; bạn có thể dán vào tab Console của DevTools để xem số thật.
Ví dụ thực tế: Trang bán hàng có thể đọc
screen.widthđể quyết định tải ảnh to hay nhỏ. Tuy nhiên, cách chuẩn hơn là dùng thiết kế đáp ứng (media query) mà bạn đã học ở buổi 9.
Đối tượng vị trí (location)
Đối tượng vị trí (location) dùng để lấy địa chỉ của Web hiện tại hoặc điều hướng trình duyệt sang một trang khác; cho phép thao tác với URL, điều hướng giữa các trang và tải lại trang. Các thuộc tính thông dụng:
| Thuộc tính | Ý nghĩa |
|---|---|
href |
URL của trang hiện tại |
hostname |
tên miền của trang hiện tại |
pathname |
đường dẫn và tên tệp của trang hiện tại |
protocol |
giao thức, có kèm dấu hai chấm (http:, https:) |
origin |
giao thức, tên miền và cổng |
Ví dụ Website triển khai tại https://minh-blogs.web.app thì location.protocol là https:, location.hostname là minh-blogs.web.app, location.origin là https://minh-blogs.web.app. Để bạn chạy được ngay (khung chạy thử không có địa chỉ thật), ví dụ dưới dùng đối tượng URL — nó có cùng các thuộc tính với location:
const dd = new URL("https://minh-blogs.web.app/bai-viet/12?id=5#binh-luan");
console.log("href :", dd.href);
console.log("protocol:", dd.protocol);
console.log("hostname:", dd.hostname);
console.log("pathname:", dd.pathname);
console.log("origin :", dd.origin);Với location thật, bạn còn có thể điều hướng: location.href = "https://..." chuyển sang trang khác, location.reload() tải lại trang. Hai lệnh này không có nút chạy thử vì khung thử nghiệm không cho điều hướng.
Đối tượng lịch sử (history)
Đối tượng lịch sử (history) chứa lịch sử duyệt trang. Nếu trước đó người dùng đã ghé 5 trang rồi mới sang trang của bạn thì history.length trả về 6 (tính cả trang đang mở). Vì bảo vệ quyền riêng tư nên bạn không biết đó là 5 trang nào, nhưng vẫn điều hướng tới chúng được:
back(): quay lại trang trước đó.forward(): vào trang sau đó (sau khiback).go(n): di chuyểnnbước trong lịch sử;nâm là lùi,ndương là tiến,go(0)tải lại trang hiện tại.
Các phương thức này hoạt động giống hai nút back/forward trên thanh điều hướng của trình duyệt.
history.back(); // như bấm nút Back
history.go(-2); // lùi 2 trang
history.go(0); // tải lại trang hiện tạiĐối tượng trình duyệt (navigator)
Đối tượng trình duyệt (navigator) xác định thông tin trình duyệt: appName (tên ứng dụng), appCodeName (tên mã), appVersion (phiên bản), language (ngôn ngữ), cookieEnabled (true nếu cookie được bật) và geolocation (vị trí địa lý).
Cảnh báo: Thông tin từ
navigatorthường có thể sai lệch vì các trình duyệt khác nhau có thể dùng cùng tên, và chính người dùng cũng có thể đổi dữ liệu này. Đừng dựa vào nó để quyết định chức năng quan trọng; hãy kiểm tra tính năng (if (navigator.geolocation)).
Thuộc tính geolocation trả về vị trí địa lý nếu người dùng cho phép; phương thức getCurrentPosition() trả về vị trí. Ví dụ lấy vĩ độ và kinh độ:
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
let vitri = "Vĩ độ: " + position.coords.latitude + " Kinh độ: " + position.coords.longitude;
console.log(vitri)
});
} else {
console.log("Không thể truy cập vào vị trí của người dùng")
}Trình duyệt sẽ hỏi người dùng có cấp quyền vị trí cho Website hay không; chỉ khi người dùng chấp thuận, getCurrentPosition() mới lấy được vị trí. Ví dụ này cần quyền của người dùng nên chỉ để minh họa.
Đối tượng cookieStore và cookie
Đối tượng cookieStore cho phép lưu thông tin người dùng trong các trang Web. Cookie được lưu dưới dạng các cặp tên/giá trị: lần đầu người dùng vào trang, tên của họ lưu vào cookie; lần sau cookie "nhớ" và trang chào đúng tên. Lưu ý cookieStore là giao diện lập trình mới, hiện chỉ các trình duyệt nhân Chromium hỗ trợ; để chạy trên mọi trình duyệt vẫn phải dùng thuộc tính document.cookie.
Tạo cookie bằng set() (nên khai báo expires để xác định thời điểm cookie hết hạn), đọc bằng get() (trả về Promise nên dùng then() để nhận kết quả), xóa bằng delete():
cookieStore.set({
name: "ten",
value: "Phạm Dương Minh",
expires: Date.now() + 24 * 60 * 60 * 1000 // Một ngày
})
cookieStore.get("ten").then(cookie => {
console.log(cookie);
});
cookieStore.delete("ten")Cách tương thích rộng hơn là document.cookie. Ví dụ dưới ghi một cookie rồi đọc lại (bọc try/catch vì khung thử nghiệm bị cách ly có thể chặn cookie):
try {
document.cookie = "ten=Minh; expires=Fri, 31 Dec 2100 23:59:59 GMT; path=/";
console.log("document.cookie =", document.cookie);
} catch (loi) {
console.log("Trình duyệt chặn cookie trong khung này:", loi.name);
}Đối tượng lưu trữ localStorage
localStorage lưu dữ liệu tương tự cookie trên máy người dùng nhưng không có ngày hết hạn và dung lượng lớn hơn nhiều. Lưu bằng setItem(), hoặc gán như thuộc tính; đọc bằng getItem() hoặc truy cập thuộc tính; xóa bằng removeItem(). Điều cần nhớ: localStorage chỉ lưu chuỗi — số 22 sau khi lưu, lúc đọc ra là chuỗi "22". Muốn lưu đối tượng thì chuyển sang chuỗi bằng JSON.stringify() và khi đọc khôi phục bằng JSON.parse() (buổi 13 sẽ học JSON kỹ hơn).
try {
localStorage.setItem("ten", "Phạm Dương Minh");
localStorage.tuoi = 22;
console.log(localStorage.getItem("ten"));
console.log(localStorage.tuoi, typeof localStorage.tuoi);
const sv = { ma: "SV01", diem: [8, 9] };
localStorage.setItem("sv", JSON.stringify(sv));
const doc = JSON.parse(localStorage.getItem("sv"));
console.log(doc.ma, doc.diem.length);
localStorage.removeItem("ten");
localStorage.removeItem("tuoi");
console.log("Sau khi xóa:", localStorage.getItem("ten"));
} catch (loi) {
console.log("Khung này không cho dùng localStorage:", loi.name);
}Đối tượng lưu trữ sessionStorage
sessionStorage tương tự localStorage ngoại trừ việc chỉ lưu dữ liệu trong một phiên: dữ liệu bị xóa khi người dùng đóng tab trình duyệt đó. Ví dụ đếm số lần người dùng bấm nút trong phiên hiện tại:
<button id="nut">Bấm tôi</button>
<p id="result">Chưa bấm lần nào</p>
<script>
var dem = 0; // dự phòng khi trình duyệt chặn sessionStorage
document.getElementById("nut").addEventListener("click", function () {
try {
if (sessionStorage.solanbam) {
sessionStorage.solanbam = Number(sessionStorage.solanbam) + 1;
} else {
sessionStorage.solanbam = 1;
}
dem = sessionStorage.solanbam;
} catch (loi) {
dem++;
}
document.getElementById("result").innerHTML = "Bạn đã bấm " + dem + " lần trong phiên làm việc này";
});
</script>| Cookie | localStorage | sessionStorage | |
|---|---|---|---|
| Hết hạn | Theo expires |
Không | Khi đóng tab |
| Dung lượng | Nhỏ | Lớn | Lớn |
| Kiểu dữ liệu | Cặp tên/giá trị (chuỗi) | Chuỗi | Chuỗi |
Mở rộng: Không lưu mật khẩu hay thông tin nhạy cảm vào localStorage/sessionStorage: mã JavaScript bất kỳ chạy trên cùng trang đều đọc được chúng.
2. Đối tượng tài liệu
Giáo trình: Chương 6 — mục "Đối tượng tài liệu" (Tìm phần tử HTML; Thay đổi thuộc tính).
Ở phần 1 bạn đã biết các thuộc tính quan trọng của window. Một trong số đó là đối tượng tài liệu (document): nó đại diện cho toàn bộ cấu trúc và nội dung của trang Web đang hiển thị trong trình duyệt. document cung cấp các phương thức và thuộc tính để truy xuất, thay đổi và tương tác với các phần tử HTML, đồng thời quản lý sự kiện và cập nhật DOM (Document Object Model — mô hình đối tượng tài liệu) để đồng bộ các thay đổi lên trang. Nó là trung tâm của mô hình DOM, giúp xây dựng ứng dụng Web động và có tương tác.
<html> <head><title>Trang</title></head> <body> <h1 id="tt">Xin chào</h1> <ul><li>HTML</li><li>CSS</li></ul> </body> </html>
- tài liệunodeType 9, gốc của cây
- phần tửnodeType 1, nodeName = tên thẻ
- văn bảnnodeType 3, nodeName "#text"
- thuộc tínhđọc bằng getAttribute()
- Mã xuống dòng, thụt lề sẽ sinh thêm nút
#textchứa khoảng trắng giữa các thẻ; hình đã lược bỏ cho gọn.
ul.parentNode→<body>body.firstChild→<h1>h1.nextSibling→<ul>ul.lastChild→<li> thứ haili2.previousSibling→<li> thứ nhất- nodeName
- "H1"
- nodeValue
- null
- nodeType
- 1
- nodeName
- "#text"
- nodeValue
- "Xin chào"
- nodeType
- 3
- nodeName
- "#document"
- nodeValue
- null
- nodeType
- 9
Trình duyệt biến mã HTML thành cây nút; JavaScript đi trên cây bằng parentNode, firstChild, lastChild, nextSibling, previousSibling và đọc nodeName, nodeValue, nodeType.
Cây trên cho thấy trình duyệt biến mã HTML thành một cây DOM: document là gốc, bên dưới là html, rồi head và body, cứ thế lồng nhau xuống từng thẻ và từng đoạn văn bản. JavaScript đi trên cây này để tìm và sửa trang.
Bằng JavaScript bạn có thể tương tác với phần tử trong tài liệu HTML để:
- Thay đổi các phần tử HTML;
- Thay đổi các thuộc tính HTML;
- Thay đổi các định dạng (CSS) của các phần tử HTML;
- Xóa/thêm các phần tử/thuộc tính HTML;
- Tạo các sự kiện mới trong trang.
Ví dụ sau thay đổi nội dung của phần tử <p> có id là welcome. Cách phổ biến nhất để truy cập một phần tử là dùng id của nó: getElementById() tìm phần tử có id bằng "welcome", còn innerHTML là thuộc tính cần thay đổi.
<html>
<body>
<p id="welcome"></p>
<script>
document.getElementById("welcome").innerHTML = "Chào buổi sáng";
</script>
</body>
</html>Lưu ý: Đoạn
<script>nằm sau phần tử<p>. Nếu đặt script trước khi trình duyệt dựng xong phần tử,getElementById()trả vềnullvà lệnh gán sẽ báo lỗi. Đây là lý do mã thường đặt cuối<body>hoặc bọc trong sự kiệnDOMContentLoaded(xem phần 3).
2.1. Tìm phần tử HTML
Muốn thay đổi một phần tử, trước hết phải xác định được nó. Đối tượng document cung cấp các phương thức để tìm:
| Phương thức | Tìm theo | Kết quả trả về |
|---|---|---|
getElementById('id') |
id |
Một phần tử (hoặc null) |
getElementsByTagName('tên-thẻ') |
tên thẻ (p, a, div...) |
HTMLCollection |
getElementsByClassName('tên-lớp') |
tên lớp (class) |
HTMLCollection |
querySelectorAll('bộ-chọn-CSS') |
bộ chọn CSS | NodeList (mọi phần tử khớp) |
querySelector('bộ-chọn-CSS') |
bộ chọn CSS | Phần tử khớp đầu tiên (hoặc null) |
Theo id: getElementById()
var phanTu = document.getElementById('id-của-phần-tử');id là duy nhất trong trang nên kết quả là một phần tử đơn lẻ.
Theo tên thẻ: getElementsByTagName()
Tên phần tử HTML chính là tên các thẻ như p, a, div... Cú pháp var phần-tử = document.getElementsByTagName('tên-phần-tử');. Trong một trang có thể có nhiều thẻ giống nhau (ví dụ hai thẻ div) nên phương thức này trả về một tập hợp (HTMLCollection) các phần tử chứ không phải một đối tượng đơn lẻ. Muốn lấy thẻ input thứ nhất ta truyền chỉ số 0.
Ví dụ của giáo trình dùng document.write() để in giá trị; bản dưới ghi ra phần tử <p> bằng textContent cho an toàn:
<input type="text" id="name" value="test">
<input type="password" id="password">
<p id="kq"></p>
<script>
var element = document.getElementsByTagName('input');
console.log("Số thẻ input:", element.length);
document.getElementById("kq").textContent = "Giá trị ô đầu tiên: " + element[0].value;
</script>Cảnh báo: Phương thức
document.write()trong ví dụ gốc chỉ nên dùng để thử nhanh: nếu được gọi sau khi trang đã tải xong, nó sẽ xóa sạch nội dung trang hiện có. Trong thực tế hãy dùnginnerHTMLhoặctextContentđể hiển thị kết quả.
Theo tên lớp: getElementsByClassName()
Để tìm theo tên lớp (class), dùng getElementsByClassName('tên-lớp'). Giống tìm theo tên thẻ, kết quả là một HTMLCollection chứ không phải mảng: có thể dùng chỉ số và thuộc tính length giống mảng, nhưng không dùng được các phương thức của mảng như map() hay forEach(). Muốn dùng phải chuyển sang mảng bằng Array.from().
<ul>
<li class="mon">HTML</li>
<li class="mon">CSS</li>
<li class="mon">JavaScript</li>
<li>Không có lớp</li>
</ul>
<script>
var ds = document.getElementsByClassName('mon');
console.log("length =", ds.length, "| có map?", typeof ds.map);
console.log("phần tử đầu:", ds[0].textContent);
var mang = Array.from(ds); // chuyển HTMLCollection thành mảng thật
var ten = mang.map(function (li) { return li.textContent.toUpperCase(); });
console.log(ten.join(", "));
</script>Theo bộ chọn CSS: querySelectorAll() và querySelector()
Phương thức querySelectorAll() tìm phần tử theo bộ chọn CSS (bạn đã học ở buổi 6). Nó trả về một NodeList gồm mọi phần tử khớp; nếu chỉ cần phần tử đầu tiên thì dùng querySelector(). Ví dụ tìm tất cả các phần tử <div> có class là note: var element = document.querySelectorAll("div.note");.
<div class="note">Ghi chú 1</div>
<div class="note">Ghi chú 2</div>
<p class="note">Đoạn có lớp note</p>
<script>
var tatca = document.querySelectorAll("div.note");
console.log("Số div.note:", tatca.length);
tatca.forEach(function (d) { d.style.color = "crimson"; }); // NodeList có forEach
var dau = document.querySelector(".note");
console.log("Phần tử .note đầu tiên:", dau.tagName, dau.textContent);
console.log("Không khớp thì:", document.querySelector("#khong-co"));
</script>Mẹo: Với bộ chọn CSS,
document.querySelector("#id")tương đươnggetElementById("id"),querySelectorAll("p")tương đươnggetElementsByTagName("p"). Khác biệt thú vị:getElementsBy...trả về tập hợp sống (tự cập nhật khi DOM đổi), cònquerySelectorAlltrả về tập tĩnh (ảnh chụp tại thời điểm gọi).
Bạn cũng có thể thử các phép tìm kiếm trực tiếp trên mô phỏng của giáo trình ("Cây DOM và querySelector"):
Cây DOM tương tác
chọn nút · tìm phần tử · thêm, sửa, xóa nútXin chào
- Trang chủ
- Giới thiệu
Ghi chú
Bấm vào phần tử trên trang cũng chọn được nút tương ứng.
<ul#list.menu>nodeName = UL · nodeType = 1 · nodeValue = nullMô phỏng cho bạn xem cây DOM của một trang mẫu; hãy gõ các bộ chọn khác nhau và quan sát phần tử nào được tô sáng, so sánh #id, .lớp và tên thẻ.
Vừa học xong cách tìm phần tử trong tài liệu, hãy làm các bài sau để tự tay tính số phần tử khớp theo id, thẻ, lớp và bộ chọn kết hợp:
2.2. Thay đổi thuộc tính
Để thay đổi giá trị của một thuộc tính phần tử HTML bất kỳ, dùng cú pháp element.attributeName = "new value";. Ví dụ để lấy và thay đổi nội dung bên trong một thẻ, truy cập thuộc tính innerHTML: document.getElementById("content").innerHTML = "<h1>Nội dung</h1>"; (chuỗi gán có thể chứa cả thẻ HTML).
Ví dụ của giáo trình: viết chương trình để khi bấm vào một button (nút bấm) thì nó chuyển thành một textbox (ô nhập):
<html>
<body>
<script>
function change() {
var object = document.getElementById("object");
var type = object.type;
object.type = 'text';
}
</script>
<input type="button" value="Click here" onclick="change()" id="object" />
</body>
</html>Hãy bấm vào nút trong khung xem trước: thuộc tính type đổi từ button sang text nên phần tử hiển thị khác hẳn dù vẫn là thẻ input ban đầu.
Lưu ý:
innerHTMLphân tích chuỗi như HTML nên không nên gán dữ liệu do người dùng nhập (nguy cơ chèn mã độc XSS). Nếu chỉ hiển thị văn bản thuần, dùngtextContent.
<p id="a"></p>
<p id="b"></p>
<script>
var chuoi = "<b>Đậm</b> & <i>nghiêng</i>";
document.getElementById("a").innerHTML = chuoi; // hiểu là HTML
document.getElementById("b").textContent = chuoi; // hiện nguyên văn
console.log(document.getElementById("a").children.length, document.getElementById("b").children.length);
</script>Thuộc tính style
Thuộc tính style cũng là một thuộc tính của các phần tử HTML: nó cho phép đọc và ghi các khai báo CSS nội tuyến của phần tử. Để thay đổi định dạng, viết document.getElementById("object").style.cssName = 'something';.
Quy tắc đặt tên: thuộc tính CSS có dấu gạch ngang như font-size, line-height, margin-bottom thì trong style có tên fontSize, lineHeight, marginBottom — bỏ dấu gạch ngang và viết hoa ký tự đầu của chữ thứ hai (kiểu camelCase).
<p id="tieude">Tiêu đề thử nghiệm</p>
<script>
var p = document.getElementById("tieude");
p.style.color = "white";
p.style.backgroundColor = "teal"; // background-color
p.style.fontSize = "24px"; // font-size
p.style.marginBottom = "8px"; // margin-bottom
console.log(p.getAttribute("style"));
</script>Cảnh báo:
element.stylechỉ đọc và ghi được phần CSS nội tuyến (viết trực tiếp trong thuộc tínhstylecủa thẻ). Những định dạng khai báo trong tệp CSS hay trong thẻ<style>sẽ cho giá trị rỗng; muốn đọc chúng phải dùng hàmwindow.getComputedStyle().
<style>
.to { font-size: 20px; }
</style>
<p id="doan" class="to">Cỡ chữ lấy từ thẻ style</p>
<script>
var p = document.getElementById("doan");
console.log("style.fontSize =", JSON.stringify(p.style.fontSize));
console.log("getComputedStyle size =", window.getComputedStyle(p).fontSize);
</script>Dòng đầu in chuỗi rỗng "", dòng sau in 20px — đó chính là khác biệt giữa style (chỉ nội tuyến) và getComputedStyle() (giá trị cuối cùng sau khi áp mọi CSS).
Ví dụ thực tế: Trên trang tin tức, nút "A+" tăng cỡ chữ: đọc cỡ hiện tại bằng
getComputedStyle(), cộng thêm 2px rồi ghi lại bằngstyle.fontSize.
Mở rộng: Ngoài
style, phần tử còn cóclassListđể đổi định dạng gọn hơn:classList.add("noi-bat"),classList.remove("noi-bat"),classList.toggle("noi-bat")(có lớp thì bỏ, chưa có thì thêm),classList.contains("noi-bat"). Cách này giữ CSS trong tệp.css, JavaScript chỉ bật/tắt lớp.
<style>
.noi-bat { background: gold; font-weight: bold; }
</style>
<p id="dong" class="ghichu">Một dòng ghi chú</p>
<script>
var d = document.getElementById("dong");
d.classList.toggle("noi-bat");
console.log(d.className, "|", d.classList.contains("noi-bat"));
d.classList.toggle("noi-bat");
console.log(d.className, "|", d.classList.contains("noi-bat"));
</script>Vừa học xong cách đổi nội dung, thuộc tính và lớp CSS của phần tử, hãy làm các bài sau:
3. Xử lý sự kiện
Giáo trình: Chương 6 — mục "Xử lý sự kiện" (Gắn sự kiện vào DOM; Lan truyền sự kiện; Đối tượng sự kiện).
3.1. Gắn sự kiện vào DOM
JavaScript có thể được thực thi khi xảy ra sự kiện (event). Một sự kiện là một hành động nào đó tác động lên các phần tử HTML. Ví dụ khi người dùng nhấp vào phần tử HTML:
<!DOCTYPE html>
<html>
<body>
<h1 onclick="changeText(this)">Click on this text!</h1>
<script>
function changeText(id) {
id.innerHTML = "Ooops!";
}
</script>
</body>
</html>Từ khóa this được truyền vào hàm changeText() chính là phần tử <h1> vừa được nhấp, nhờ đó hàm thay đổi được nội dung của chính phần tử đó. Hãy bấm vào tiêu đề trong khung xem trước.
Ví dụ về các sự kiện HTML: khi người dùng nhấp chuột; khi một trang Web đã được tải; khi một hình ảnh đã được tải; khi chuột di chuyển qua một phần tử; khi một trường đầu vào được thay đổi; khi một biểu mẫu HTML được gửi; khi người dùng nhấn một phím.
| Sự kiện | Mô tả |
|---|---|
onchange |
Khi một phần tử HTML thay đổi |
onclick |
Khi người dùng bấm lên một phần tử HTML |
onmouseover |
Khi người dùng di chuột trên một phần tử HTML |
onmouseout |
Khi người dùng di chuột ra khỏi một phần tử HTML |
onkeydown |
Khi người dùng bấm bàn phím |
onload |
Khi trình duyệt kết thúc việc tải trang |
Các sự kiện onload và onunload được kích hoạt khi người dùng vào hoặc rời trang. onload có thể dùng để kiểm tra loại và phiên bản trình duyệt của người truy cập, từ đó tải phiên bản trang phù hợp; onload/onunload còn dùng để đọc (hoặc ghi) cookie. onchange thường dùng để kiểm tra dữ liệu người dùng nhập. onmouseover và onmouseout dùng để kích hoạt chức năng khi người dùng di chuột vào và ra khỏi phần tử.
Lưu ý: Cần phân biệt sự kiện
loadcủawindowvới sự kiệnDOMContentLoadedcủadocument.DOMContentLoadedxảy ra ngay khi cây DOM đã dựng xong; cònloadchỉ xảy ra khi mọi tài nguyên như ảnh, tệp CSS, tệp JavaScript đều đã tải xong. Vì vậy mã khởi tạo giao diện thường gắn vàoDOMContentLoadedđể chạy sớm hơn.
<p id="tt">Đang tải...</p>
<script>
document.addEventListener("DOMContentLoaded", function () {
console.log("1) DOMContentLoaded: cây DOM đã dựng xong");
document.getElementById("tt").textContent = "DOM sẵn sàng";
});
window.addEventListener("load", function () {
console.log("2) load: mọi tài nguyên đã tải xong");
});
console.log("0) Mã script chạy tới dòng cuối");
</script>addEventListener()
Phương thức addEventListener() gắn một trình xử lý sự kiện (event handler/listener) cho một phần tử HTML. Cú pháp:
element.addEventListener(event, function, useCapture);- Tham số thứ nhất là loại sự kiện (như
"click","mousedown"hoặc bất kỳ sự kiện DOM nào khác — lưu ý không có chữon). - Tham số thứ hai là hàm xử lý được gọi khi sự kiện xảy ra.
- Tham số thứ ba là giá trị boolean xác định dùng cơ chế bubbling hay capturing. Tham số này là tùy chọn.
Ví dụ giáo trình dùng nút bấm hiển thị ngày giờ (innerHTML = new Date()). Bản chạy thử dưới đây đếm số lần bấm để kết quả không phụ thuộc đồng hồ:
<!DOCTYPE html>
<html>
<body>
<button id="myBtn">Bấm vào đây</button>
<script>
var lan = 0;
document.getElementById("myBtn").addEventListener("click", displayDate);
function displayDate() {
lan++;
document.getElementById("myBtn").innerHTML = "Đã bấm " + lan + " lần";
}
</script>
</body>
</html>addEventListener() gắn thêm một trình xử lý mà không ghi đè trình xử lý hiện có, nên có thể thêm nhiều trình xử lý cho cùng một phần tử. Bạn cũng có thể lắng nghe sự kiện trên bất kỳ đối tượng DOM nào, không chỉ phần tử HTML (ví dụ đối tượng window). Khi dùng addEventListener(), mã JavaScript được tách khỏi mã HTML, giúp mã dễ đọc hơn và cho phép thêm trình xử lý ngay cả khi không sửa được tệp HTML.
<button id="myBtn">Bấm</button>
<script>
var element = document.getElementById("myBtn");
element.addEventListener("click", function () { alert("Hello World!"); });
element.addEventListener("click", function () { console.log("Trình xử lý thứ hai vẫn chạy"); });
element.onclick = function () { console.log("onclick (1)"); };
element.onclick = function () { console.log("onclick (2) ghi đè (1)"); };
element.click(); // mô phỏng người dùng bấm
</script>Quan sát bảng console: cả hai trình xử lý addEventListener đều chạy, còn với thuộc tính onclick thì lần gán sau ghi đè lần gán trước.
Lan truyền sự kiện: capturing → target → bubbling
bấm vào hộp (hoặc nút) để phát sự kiện clickngoai.addEventListener("click", e => { ghiLog(e); }, false); giua.addEventListener("click", e => { ghiLog(e); }, false); trong.addEventListener("click", e => { ghiLog(e); }, false);
Đổi cấu hình sẽ xóa lượt chạy hiện tại; bấm lại để phát sự kiện với cấu hình mới.
Trong mô phỏng, bấm vào các phần tử lồng nhau và xem trình xử lý nào được gọi — bạn sẽ dùng lại mô phỏng này ở phần sau để hiểu lan truyền sự kiện.
3.2. Lan truyền sự kiện
Lan truyền sự kiện (event propagation) là cơ chế quy định thứ tự xử lý sự kiện giữa các phần tử lồng nhau khi một sự kiện xảy ra. Nếu có phần tử <p> bên trong phần tử <div> và người dùng nhấp vào <p>, thì sự kiện "nhấp chuột" của phần tử nào được xử lý trước?
documentgốc của mọi sự kiện<body><div id="ngoai">phần tử cha<p id="trong">phần tử đích- 1
documentđi xuống từ ngoài cùngCapturing ↓ - 2
bodytiếp tục đi vào trongCapturing ↓ - 3
div#ngoaisát phần tử đíchCapturing ↓ - 4
p#trongnơi người dùng bấm thậtTarget ● - 5
div#ngoainổi ngược lênBubbling ↑ - 6
bodytiếp tục nổi lênBubbling ↑ - 7
documenttới ngoài cùng, kết thúcBubbling ↑
el.addEventListener('click', fn, false) // false (mặc định): bubbling el.addEventListener('click', fn, true) // true: capturing
- e.target luôn là
<p>, nơi bấm thật - e.currentTarget là phần tử đang chạy trình xử lý (
<div>khi nổi tới nó)
Gọi trong trình xử lý của <div>: sự kiện dừng tại đó, các lớp ngoài hơn không nhận được nữa.
Mỗi sự kiện đi qua ba pha: xuống từ ngoài vào (capturing), tới đúng phần tử (target), rồi nổi ngược ra ngoài (bubbling). Mặc định trình xử lý chạy ở pha bubbling.
Hình mô tả mỗi sự kiện lần lượt đi qua ba pha: pha capturing đi từ phần tử ngoài cùng vào trong, pha target tại đúng phần tử phát sinh sự kiện, rồi pha bubbling đi ngược từ trong ra ngoài. Người lập trình chọn xử lý ở pha nào, tương ứng với hai cách lan truyền:
- Kiểu bubbling (nổi bọt): sự kiện của phần tử bên trong được xử lý trước, sau đó đến bên ngoài — sự kiện nhấp của
<p>xử lý trước, rồi mới đến<div>. - Kiểu capturing (bắt từ ngoài vào): sự kiện của phần tử ngoài cùng xử lý trước, rồi mới đến phần tử bên trong —
<div>trước, sau đó<p>.
Với addEventListener(), chỉ định kiểu lan truyền bằng tham số useCapture. Giá trị mặc định là false → dùng bubbling; đặt thành true → dùng capturing. Ví dụ dùng kiểu capturing:
document.getElementById("myP").addEventListener("click", myFunction, true);
document.getElementById("myDiv").addEventListener("click", myFunction, true);Chạy thử để thấy thứ tự thực sự. Mỗi hàm xử lý ghi tên phần tử vào bảng console:
<div id="myDiv" style="padding:16px;background:#dbeafe">
div ngoài
<p id="myP" style="background:#fde68a;margin:0">p bên trong (bấm vào đây)</p>
</div>
<script>
var div = document.getElementById("myDiv");
var p = document.getElementById("myP");
// Kiểu bubbling (mặc định, useCapture = false)
div.addEventListener("click", function () { console.log("bubbling: div"); });
p.addEventListener("click", function () { console.log("bubbling: p"); });
// Kiểu capturing (useCapture = true)
div.addEventListener("click", function () { console.log("capturing: div"); }, true);
p.addEventListener("click", function () { console.log("capturing: p"); }, true);
p.click(); // mô phỏng người dùng bấm vào p
</script>Kết quả theo đúng thứ tự: capturing: div → capturing: p → bubbling: p → bubbling: div. Chú ý: ở chính phần tử đích (target), các hàm xử lý chạy theo thứ tự đăng ký (trình duyệt hiện đại cho capturing chạy trước), còn trên các phần tử cha thì capturing luôn đi trước bubbling.
Mẹo: Phần lớn mã thực tế chỉ dùng bubbling (mặc định). Nhờ bubbling bạn còn có thể gắn một trình xử lý lên phần tử cha (như
<ul>) để xử lý bấm vào mọi phần tử con (<li>) — kỹ thuật gọi là event delegation (ủy quyền sự kiện).
Vừa học xong cơ chế lan truyền sự kiện, hãy làm các bài sau để dự đoán đường đi và thứ tự các listener:
3.3. Đối tượng sự kiện
Khi một sự kiện xảy ra, trình duyệt truyền cho hàm xử lý một đối tượng sự kiện chứa thông tin về sự kiện đó. Đối tượng này thường đặt tên là event hoặc viết tắt e:
<button id="nut">Nút</button>
<script>
document.getElementById("nut").addEventListener("click", function (e) {
console.log(e.type); // "click"
console.log(e.target.id); // phần tử đã phát sinh sự kiện (id = nut)
console.log(e.target === e.currentTarget);
});
document.getElementById("nut").click();
</script>Hai phương thức quan trọng nhất của đối tượng sự kiện:
preventDefault(): ngăn hành vi mặc định của trình duyệt. Ví dụ ngăn biểu mẫu tự gửi đi để kiểm tra dữ liệu trước, hoặc ngăn một liên kết chuyển trang.stopPropagation(): dừng việc lan truyền sự kiện lên các phần tử cha; dùng khi phần tử con và phần tử cha cùng có hàm xử lý cho một loại sự kiện.
Ví dụ giáo trình: kiểm tra họ tên trước khi gửi biểu mẫu. Bản chạy thử dưới đây phát một sự kiện submit để bạn xem hành vi mặc định có bị chặn hay không:
<form id="dangky">
<input id="hoten" type="text" value="">
<button type="submit">Gửi</button>
</form>
<script>
document.getElementById("dangky").addEventListener("submit", function (e) {
if (document.getElementById("hoten").value === "") {
e.preventDefault();
alert("Vui lòng nhập họ tên");
}
});
var sk = new Event("submit", { cancelable: true, bubbles: true });
document.getElementById("dangky").dispatchEvent(sk);
console.log("Đã chặn gửi biểu mẫu?", sk.defaultPrevented);
</script>Còn đây là stopPropagation(): phần tử con dừng sự kiện nên phần tử cha không nhận được.
<div id="cha"><button id="con">Con</button></div>
<script>
document.getElementById("cha").addEventListener("click", function () {
console.log("cha nhận click");
});
document.getElementById("con").addEventListener("click", function (e) {
console.log("con nhận click");
e.stopPropagation(); // thử xóa dòng này: cha cũng sẽ nhận click
});
document.getElementById("con").click();
</script>Gỡ trình xử lý bằng removeEventListener()
Khi không cần theo dõi một sự kiện nữa, dùng removeEventListener() với đúng tên sự kiện và đúng hàm xử lý đã đăng ký. Vì vậy hàm xử lý phải là một hàm có tên; hàm ẩn danh viết trực tiếp trong addEventListener() sẽ không thể gỡ bỏ được.
<button id="nut">Bấm</button>
<script>
var nut = document.getElementById("nut");
function chao() { console.log("Xin chào"); }
nut.addEventListener("click", chao);
nut.click(); // in "Xin chào"
nut.removeEventListener("click", chao);
nut.click(); // không in gì nữa
console.log("Xong");
</script>Cảnh báo:
nut.addEventListener("click", function () {...})rồinut.removeEventListener("click", function () {...})với hàm viết lại không gỡ được, vì hai hàm ẩn danh là hai đối tượng khác nhau (xem lại tham chiếu ở buổi 11).
4. Nút (Node)
Giáo trình: Chương 6 — mục "Nút (Node)" (Mối quan hệ giữa các nút; Các thuộc tính của các nút; Tạo các phần tử HTML mới; Xóa các phần tử HTML hiện có).
Theo tiêu chuẩn HTML DOM của W3C, mọi thành phần trong tài liệu HTML đều là một nút (node). Trong đó một số nút chính là các phần tử HTML, số còn lại thì không:
| Loại nút | Mô tả | nodeType |
|---|---|---|
| Nút tài liệu (document node) | Toàn bộ tài liệu | 9 |
| Nút phần tử (element node) | Mỗi phần tử HTML | 1 |
| Nút thuộc tính (attribute node) | Mỗi thuộc tính HTML | 2 |
| Nút văn bản (text node) | Văn bản bên trong các phần tử HTML | 3 |
| Nút chú thích (comment node) | Mỗi chú thích (comment) | 8 |
Với HTML DOM, mọi nút trong cây nút có thể truy cập bằng JavaScript; các nút mới có thể được tạo, và tất cả các nút có thể được sửa đổi hoặc xóa.
Lưu ý: Riêng nút thuộc tính, từ chuẩn DOM cấp 4 không còn được coi là một nút thực thụ trong cây; thuộc tính của phần tử được truy cập qua các phương thức
getAttribute()vàsetAttribute().
<p id="lop" title="Gợi ý">Xin chào</p>
<script>
var p = document.getElementById("lop");
console.log(p.getAttribute("title"));
p.setAttribute("title", "Đã đổi");
console.log(p.getAttribute("title"));
console.log(p.hasAttribute("href"));
p.removeAttribute("title");
console.log(p.getAttribute("title")); // null khi không còn thuộc tính
</script>4.1. Mối quan hệ giữa các nút
Các nút trong cây có quan hệ phân cấp với nhau. Các thuật ngữ cha (parent), con (child) và anh chị em (sibling) được dùng để mô tả:
- Trong cây nút, nút trên cùng gọi là nút gốc (root).
- Mỗi nút có đúng một nút cha, ngoại trừ nút gốc (không có nút cha).
- Một nút có thể có nhiều nút con.
- Anh chị em (sibling) là các nút có cùng một nút cha.
Ví dụ với đoạn HTML <ul><li>A</li><li>B</li><li>C</li></ul>: ul là cha của ba li; ba li là anh chị em của nhau; li chứa A có nút con là một nút văn bản A.
Cây DOM tương tác
chọn nút · tìm phần tử · thêm, sửa, xóa nútXin chào
- Trang chủ
- Giới thiệu
Ghi chú
Bấm vào phần tử trên trang cũng chọn được nút tương ứng.
<ul#list.menu>nodeName = UL · nodeType = 1 · nodeValue = nullHãy dùng mô phỏng cây DOM để chọn một nút và đọc ra cha, con và anh em của nó, đối chiếu với mô tả ở trên.
4.2. Các thuộc tính của các nút
Có thể dùng các thuộc tính sau để điều hướng giữa các nút:
| Thuộc tính | Trả về |
|---|---|
parentNode |
Nút cha |
childNodes[nodenumber] |
Danh sách nút con (đánh chỉ số từ 0) |
firstChild |
Nút con đầu tiên |
lastChild |
Nút con cuối cùng |
nextSibling |
Nút anh/chị em kế tiếp |
previousSibling |
Nút anh/chị em liền trước |
Thuộc tính nodeName xác định tên của một nút (chỉ đọc): nút phần tử có nodeName giống tên thẻ (viết hoa, ví dụ P); nút thuộc tính là tên thuộc tính; nút văn bản luôn là #text; nút tài liệu luôn là #document.
Thuộc tính nodeValue xác định giá trị của nút: với nút phần tử là null; với nút văn bản là chính văn bản; với nút thuộc tính là giá trị thuộc tính.
Thuộc tính nodeType (chỉ đọc) trả về kiểu của nút dưới dạng số nguyên: 1 là nút phần tử, 3 là nút văn bản, 8 là nút chú thích, 9 là nút tài liệu.
<div id="goc"><!-- một chú thích --><p id="dau">Xin chào</p><p id="sau">Tạm biệt</p></div>
<script>
var goc = document.getElementById("goc");
console.log("parentNode của div:", goc.parentNode.nodeName);
console.log("số nút con:", goc.childNodes.length);
console.log("firstChild:", goc.firstChild.nodeName, goc.firstChild.nodeType);
console.log("lastChild :", goc.lastChild.nodeName);
var dau = document.getElementById("dau");
console.log("nextSibling:", dau.nextSibling.id, "| previousSibling:", dau.previousSibling.nodeName);
console.log("nodeValue phần tử:", dau.nodeValue, "| nodeValue văn bản:", dau.firstChild.nodeValue);
console.log("document:", document.nodeName, document.nodeType);
</script>Đọc kết quả: nút con đầu tiên của div là một nút chú thích (#comment, kiểu 8); nút con của <p> là nút văn bản (#text, kiểu 3) có nodeValue là chữ bên trong.
Mở rộng: Khoảng trắng và xuống dòng giữa các thẻ cũng là nút văn bản, nên khi HTML có thụt dòng,
firstChild/nextSiblingthường trả về nút#textrỗng thay vì phần tử bạn mong. Để chỉ đi qua các phần tử, dùng bản "Element" của thuộc tính:children,firstElementChild,lastElementChild,nextElementSibling,previousElementSibling,parentElement.
<ul id="ds">
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
<script>
var ul = document.getElementById("ds");
console.log("childNodes.length =", ul.childNodes.length, "(gồm cả khoảng trắng)");
console.log("children.length =", ul.children.length, "(chỉ phần tử)");
console.log("firstChild.nodeName =", ul.firstChild.nodeName);
console.log("firstElementChild.text =", ul.firstElementChild.textContent);
var b = ul.children[1];
console.log("trước B:", b.previousElementSibling.textContent, "| sau B:", b.nextElementSibling.textContent);
console.log("cha của B:", b.parentElement.id);
</script>Vừa học xong các quan hệ giữa các nút, hãy làm các bài sau để thực hành tìm cha, con, anh em và tổ tiên trong một cây:
4.3. Tạo các phần tử HTML mới
Để thêm một phần tử mới vào HTML DOM, trước tiên phải tạo phần tử (nút phần tử), sau đó nối nó vào một phần tử hiện có. Quy trình gồm: createElement() → đặt nội dung/thuộc tính → appendChild().
<div id="div1">
<p id="p1">This is a paragraph.</p>
<p id="p2">This is another paragraph.</p>
</div>
<script>
var para = document.createElement("p");
para.innerHTML = "This is a new paragraph"
var element = document.getElementById("div1");
element.appendChild(para);
console.log("Số phần tử con của div1:", element.children.length);
</script>appendChild() luôn thêm nút mới vào cuối danh sách con. Để thêm phần tử vào trước một phần tử khác, dùng phương thức insertBefore() — nhận hai tham số: nút mới và nút con hiện có làm mốc. Ví dụ này còn tạo nút văn bản bằng createTextNode():
<div id="div1">
<p id="p1">This is a paragraph.</p>
<p id="p2">This is another paragraph.</p>
</div>
<script>
var para = document.createElement("p");
var node = document.createTextNode("This is new paragraph");
para.appendChild(node);
var element = document.getElementById("div1");
var child = document.getElementById("p1");
element.insertBefore(para, child);
console.log(element.children[0].textContent); // phần tử mới nằm đầu tiên
</script>Mẹo: Dùng
textContent(hoặccreateTextNode) cho văn bản người dùng nhập để tránh chèn mã HTML ngoài ý muốn. Tạo xong nhớ nối vào cây: phần tử vừacreateElementmà chưaappendChildthì chưa hiện trên trang.
Mở rộng: Muốn sao chép một phần tử có sẵn, dùng
cloneNode(true)(sao chép sâu: cả các nút con) hoặccloneNode(false)(chỉ riêng nó); bản sao chưa nằm trong cây cho tới khi bạnappendChild()nó.
<ul id="ds"><li class="sp">Bút <b>bi</b></li></ul>
<script>
var ds = document.getElementById("ds");
var goc = ds.firstElementChild;
var sau = goc.cloneNode(true); // sao chép sâu
var nong = goc.cloneNode(false); // chỉ thẻ li, không có con
ds.appendChild(sau);
console.log("Số li sau khi nối bản sao:", ds.children.length);
console.log("Bản sao sâu:", sau.innerHTML, "| bản nông:", JSON.stringify(nong.innerHTML));
</script>Xây dựng một danh sách từ dữ liệu bằng vòng lặp là cách dùng phổ biến nhất của createElement():
<h3>Môn học kỳ này</h3>
<ul id="ds"></ul>
<script>
var mon = ["Thiết kế Web", "Cơ sở dữ liệu", "Mạng máy tính"];
var ul = document.getElementById("ds");
mon.forEach(function (ten, i) {
var li = document.createElement("li");
li.textContent = (i + 1) + ". " + ten;
ul.appendChild(li);
});
console.log("Đã tạo", ul.children.length, "mục");
</script>4.4. Xóa các phần tử HTML hiện có
Để xóa phần tử HTML, dùng phương thức remove():
<div id="vung">
<p id="p1">This is a paragraph.</p>
<p id="p2">This is another paragraph.</p>
</div>
<script>
var elmnt = document.getElementById("p1");
elmnt.remove();
console.log("Còn lại:", document.getElementById("vung").children.length, "phần tử");
console.log("p1 còn trong tài liệu?", document.getElementById("p1"));
</script>Đối với các trình duyệt cũ không hỗ trợ remove(), phải tìm nút cha rồi gọi removeChild() để xóa phần tử con:
<div id="div1">
<p id="p1">This is a paragraph.</p>
<p id="p2">This is another paragraph.</p>
</div>
<script>
var parent = document.getElementById("div1");
var child = document.getElementById("p1");
parent.removeChild(child);
console.log("Còn lại:", parent.children.length, "->", parent.children[0].id);
</script>| Việc cần làm | Cách làm |
|---|---|
| Tạo phần tử | document.createElement("tên-thẻ") |
| Tạo nút văn bản | document.createTextNode("văn bản") |
| Thêm vào cuối | cha.appendChild(nút) |
| Thêm vào trước một nút | cha.insertBefore(nút, nútMốc) |
| Sao chép | nút.cloneNode(true) |
| Xóa chính nó | nút.remove() |
| Xóa qua nút cha | cha.removeChild(nútCon) |
Lưu ý: Khi xóa một phần tử thì toàn bộ nhánh con, cháu của nó cũng rời khỏi cây. Các trình xử lý sự kiện gắn trên phần tử bị xóa không còn nhận sự kiện từ trang.
Vừa học xong cách tạo, nhân bản và xóa phần tử, hãy làm các bài sau để mô phỏng việc dựng và sửa cây DOM:
Thực hành theo giáo trình
Giáo trình: Chương 6 — phần "Thực hành" (Bài 6.1–6.5: canvas, JSON trên Firebase, Postman, đọc/lưu dữ liệu Minh Blogs) và "Bài tập tự thực hành" (Bài 6.6–6.8). Các bài 6.1–6.5 cần canvas và giao tiếp máy chủ nên học ở buổi 13; ba bài tự thực hành 6.6, 6.7, 6.8 dùng đúng kiến thức DOM và sự kiện của buổi này.
Bài 6.6. Xử lý biểu mẫu
Mục tiêu: tạo trang có một ô nhập liệu và một nút gửi (submit). Khi người dùng nhập một chuỗi và bấm nút, hiển thị chuỗi đó lên trang; nếu không nhập thì đưa ra thông báo.
Các bước chính: (1) tạo form có input và button; (2) lắng nghe sự kiện submit của form; (3) gọi e.preventDefault() để trang không tải lại; (4) đọc value, nếu rỗng thì báo lỗi, ngược lại ghi vào một phần tử kết quả bằng textContent.
<form id="f">
<input id="chuoi" type="text" placeholder="Nhập một chuỗi">
<button type="submit">Gửi</button>
</form>
<p id="kq"></p>
<script>
var f = document.getElementById("f");
f.addEventListener("submit", function (e) {
e.preventDefault();
var v = document.getElementById("chuoi").value.trim();
if (v === "") {
document.getElementById("kq").textContent = "Vui lòng nhập một chuỗi";
console.log("Chưa nhập");
} else {
document.getElementById("kq").textContent = "Bạn đã nhập: " + v;
console.log("Đã hiển thị:", v);
}
});
// Mô phỏng: lần 1 để trống, lần 2 có dữ liệu
f.dispatchEvent(new Event("submit", { cancelable: true }));
document.getElementById("chuoi").value = "Xin chào DOM";
f.dispatchEvent(new Event("submit", { cancelable: true }));
</script>Bài 6.7. Website giới thiệu sản phẩm
Mục tiêu: tạo trang có danh sách sản phẩm; khi người dùng nhấp vào một sản phẩm, hiển thị thông tin chi tiết lấy từ một chuỗi JSON.
Các bước chính: (1) khai báo chuỗi JSON chứa thông tin các sản phẩm và dùng JSON.parse() để đổi thành mảng; (2) dùng createElement() tạo mỗi li cho từng sản phẩm; (3) gắn sự kiện click lên ul (bubbling) và đọc e.target.dataset.ma; (4) ghi chi tiết ra vùng hiển thị.
<ul id="ds"></ul>
<div id="chitiet">Hãy chọn một sản phẩm</div>
<script>
var json = '[{"ma":"b1","ten":"Bút bi","gia":5000},{"ma":"v1","ten":"Vở kẻ ngang","gia":12000}]';
var sp = JSON.parse(json);
var ds = document.getElementById("ds");
sp.forEach(function (x) {
var li = document.createElement("li");
li.textContent = x.ten;
li.dataset.ma = x.ma;
ds.appendChild(li);
});
ds.addEventListener("click", function (e) {
var x = sp.find(function (s) { return s.ma === e.target.dataset.ma; });
if (x) document.getElementById("chitiet").textContent = x.ten + " - giá " + x.gia + " đồng";
});
ds.children[1].click(); // mô phỏng bấm vào sản phẩm thứ hai
console.log(document.getElementById("chitiet").textContent);
</script>Bài 6.8. Tạo một trò chơi đơn giản
Mục tiêu: trò chơi đố vui nhiều câu hỏi: hiển thị câu hỏi và các phương án, kiểm tra người chơi chọn đúng chưa; chọn sai thì đưa ra gợi ý.
Các bước chính: (1) lưu câu hỏi trong mảng đối tượng (hoi, phuongAn, dung, goiY); (2) viết hàm hiển thị câu hiện tại, tạo nút cho từng phương án; (3) khi bấm, so sánh với đáp án đúng — đúng thì sang câu kế, sai thì hiện gợi ý.
<h3 id="cauhoi"></h3>
<div id="pa"></div>
<p id="tb"></p>
<script>
var ds = [
{ hoi: "Thẻ nào chứa mã JavaScript?", pa: ["script", "js", "code"], dung: 0, goiY: "Tên thẻ trùng với tên lệnh nhúng mã." },
{ hoi: "document.getElementById trả về gì?", pa: ["Mảng", "Một phần tử", "Chuỗi"], dung: 1, goiY: "id là duy nhất." }
];
var cau = 0;
function hienThi() {
var c = ds[cau];
document.getElementById("cauhoi").textContent = (cau + 1) + ". " + c.hoi;
var vung = document.getElementById("pa");
vung.innerHTML = "";
c.pa.forEach(function (ten, i) {
var nut = document.createElement("button");
nut.textContent = ten;
nut.addEventListener("click", function () { chon(i); });
vung.appendChild(nut);
});
}
function chon(i) {
var tb = document.getElementById("tb");
if (i === ds[cau].dung) {
tb.textContent = "Đúng!";
if (cau < ds.length - 1) { cau++; hienThi(); }
} else {
tb.textContent = "Sai. Gợi ý: " + ds[cau].goiY;
}
}
hienThi();
document.querySelector("#pa button:nth-child(2)").click(); // chọn sai
console.log(document.getElementById("tb").textContent);
</script>Mẹo: Cả ba bài đều có cùng mẫu: tìm phần tử → lắng nghe sự kiện → đọc dữ liệu → cập nhật DOM. Nắm chắc mẫu này là bạn đã làm chủ phần lớn lập trình giao diện bằng JavaScript.
Câu hỏi ôn tập
Các câu 6.1 – 6.8 trích từ "Câu hỏi ôn tập lý thuyết" của giáo trình (các câu 6.9 – 6.15 về JSON và AJAX thuộc buổi 13); sáu câu "Bổ sung" do bài giảng thêm để ôn các nội dung riêng của buổi này.
6.1. Mã lệnh JavaScript được đặt bên trong phần tử HTML nào? A. <script> B. <js> C. <javascript> D. <scripting>
Đáp án: A.
6.2. Cú pháp JavaScript chính xác để thay đổi nội dung của phần tử <p id="demo">Đây là một đoạn văn.</p>? A. #demo.innerHTML = "Chào"; B. document.getElementById("demo").innerHTML = "Chào"; C. document.getElement("p").innerHTML = "Chào"; D. document.getElementByName("p").innerHTML = "Chào";
Đáp án: B.
6.3. Đâu là vị trí chính xác để chèn JavaScript? A. Phần <body> B. Cả phần <head> và phần <body> đều đúng C. Phần <head> D. Phần <javascript>
Đáp án: B.
6.4. Cú pháp chính xác để tham chiếu đến tệp mã lệnh bên ngoài có tên "abc.js"? A. <script href="abc.js"> B. <script name="abc.js"> C. <script src="abc.js"> D. <script url="abc.js">
Đáp án: C.
6.5. Làm thế nào để viết "Hello World" trong một hộp cảnh báo? A. alertBox("Hello World"); B. alert("Hello World"); C. msgBox("Hello World"); D. msg("Hello World");
Đáp án: B.
6.6. Sự kiện nào xảy ra khi người dùng nhấp chuột vào một phần tử? A. onmouseclick B. onmouseover C. onclick D. onchange
Đáp án: C.
6.7. Đối tượng window chứa những đối tượng con nào? A. document B. navigator C. screen D. Tất cả các phương án trên đều đúng
Đáp án: D.
6.8. Dùng lệnh nào để lấy phần tử HTML trong tài liệu HTML? A. document.getValueById() B. document.getElementByName() C. document.getElementById() D. document.getElementByClassName()
Đáp án: C.
Bổ sung 1. Pha nào của sự kiện đi từ phần tử ngoài cùng vào trong, và giá trị useCapture nào bật nó?
Đáp án: Pha capturing; đặt useCapture = true. Mặc định là false (bubbling, đi từ trong ra ngoài).
Bổ sung 2. Vì sao removeEventListener() không gỡ được hàm ẩn danh?
Đáp án: Vì phải truyền đúng hàm đã đăng ký; hàm ẩn danh viết lại là một đối tượng khác, nên hàm xử lý cần có tên.
Bổ sung 3. nodeType của nút phần tử, nút văn bản, nút chú thích, nút tài liệu lần lượt là bao nhiêu?
Đáp án: 1, 3, 8, 9.
Bổ sung 4. getElementsByClassName() trả về mảng hay HTMLCollection? Muốn dùng map() thì làm sao?
Đáp án: Trả về HTMLCollection (không phải mảng); chuyển sang mảng bằng Array.from().
Bổ sung 5. Vì sao element.style.fontSize có thể rỗng dù trang hiển thị chữ cỡ 20px?
Đáp án: Vì style chỉ đọc CSS nội tuyến; cỡ chữ khai báo trong thẻ <style>/tệp CSS phải đọc bằng getComputedStyle().
Bổ sung 6. localStorage lưu được những kiểu dữ liệu nào? Muốn lưu đối tượng thì làm sao?
Đáp án: Chỉ lưu chuỗi; chuyển bằng JSON.stringify() khi lưu và JSON.parse() khi đọc.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Đặt <script> trước phần tử rồi gọi getElementById() |
Trả về null, báo lỗi Cannot set properties of null |
Đặt script cuối body hoặc dùng DOMContentLoaded |
Gọi .map()/.forEach() trực tiếp trên HTMLCollection |
Lỗi map is not a function |
Array.from(ds).map(...) |
Viết getElementById("#id") hoặc getElementsById |
Không tìm thấy phần tử | getElementById("id") (không có #) hoặc querySelector("#id") |
Viết style["font-size"]/style.font-size |
Không có tác dụng hoặc lỗi cú pháp | style.fontSize (camelCase) |
Đọc CSS từ thẻ <style> bằng element.style |
Nhận chuỗi rỗng | getComputedStyle(element) |
Dùng onclick = ... nhiều lần |
Lần sau ghi đè lần trước | addEventListener() để gắn nhiều trình xử lý |
| Gỡ sự kiện bằng hàm ẩn danh | Trình xử lý vẫn còn | Đặt tên hàm và truyền đúng hàm đó |
Gán dữ liệu người dùng nhập vào innerHTML |
Nguy cơ chèn mã độc (XSS) | Dùng textContent |
Quên appendChild() sau createElement() |
Phần tử không xuất hiện | Nối vào cây DOM |
Tin localStorage lưu được số/đối tượng |
Đọc ra là chuỗi "22", [object Object] |
JSON.stringify()/JSON.parse() |
Dùng document.write() sau khi tải xong trang |
Xóa sạch nội dung trang | Dùng innerHTML/textContent |
Tóm tắt
- window là đối tượng toàn cục của trình duyệt, chứa
document,history,location,navigator,screen,localStorage,sessionStorage... và các phương thức nhưalert(),confirm(),prompt(),setTimeout(),setInterval(),open(),scrollTo(),print(). - location điều hướng/đọc URL; history có
back(),forward(),go(n); navigator có thể sai lệch; cookie/localStorage/sessionStoragelưu dữ liệu phía người dùng (chỉ chuỗi). - document đại diện cho cây DOM: tìm phần tử bằng
getElementById,getElementsByTagName,getElementsByClassName,querySelector,querySelectorAll; đổi thuộc tính,innerHTML,style. - Sự kiện:
addEventListener(event, function, useCapture); ba pha capturing → target → bubbling;preventDefault(),stopPropagation();removeEventListener()cần hàm có tên. - Nút (Node): quan hệ cha – con – anh em;
nodeName,nodeValue,nodeType; tạo bằngcreateElement()+appendChild()/insertBefore(); xóa bằngremove()/removeChild().