Câu 1Nếu khai báo let count = 0; bên trong hàm truyền cho setInterval và cho dừng khi đếm đến 3, điều gì sẽ xảy ra?
Hẹn giờ và ngày tháng — setTimeout / Date / Intl
Hẹn giờ gọi hàm bằng setTimeout, dừng setInterval, tháng tính từ 0 của Date, hiển thị ngày bằng Intl.DateTimeFormat và đếm số ngày giữa hai mốc.
Đóng thông báo sau 3 giây, hay giảm số giây còn lại sau mỗi giây: những việc này không viết được nếu code chỉ chạy lần lượt từ trên xuống. Khi ngày tháng vẫn chỉ là một chuỗi, bạn cũng không thể hiển thị thứ trong tuần cạnh ngày đăng hay đếm số ngày còn lại đến hạn chót.
Bài này nói về setTimeout và setInterval để tạo bộ hẹn giờ, cùng Date và Intl.DateTimeFormat để làm việc với ngày giờ.
Lên lịch để gọi sau — setTimeout và clearTimeout
Giả sử một trang chia sẻ ảnh hiện thông báo "Đã tải lên xong" và bạn muốn đóng nó sau 3 giây. Code chạy lần lượt từ trên xuống không có cách nào nói "đợi 3 giây rồi mới chạy tiếp", còn nếu dùng một vòng for chạy rỗng để chờ thì trong lúc đó trang sẽ không phản hồi thao tác nhấn chuột và cũng không cập nhật giao diện.
setTimeout(hàm, mili giây) chỉ lên lịch thời điểm gọi hàm rồi chuyển ngay sang dòng tiếp theo. Giá trị trả về là timer ID, cùng loại bạn đã dùng cho timeout của fetch; truyền ID này vào clearTimeout là lịch hẹn bị hủy.
// Trả về một Promise được fulfilled sau ms mili giây (là hàm delay trong bài về Promise, bỏ đi value)
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Lên lịch đóng thông báo sau 100ms rồi chạy ngay dòng tiếp theo (trên trang thật thường là 3000ms)
setTimeout(() => console.log("Đã đóng thông báo"), 100); // Đã đóng thông báo (thứ 2)
console.log("Đã tải lên xong"); // Đã tải lên xong (thứ 1)
// Người dùng đã rời trang nên hủy lịch hiển thị ảnh gợi ý. Dù đã qua 100ms, hàm này cũng không được gọi
const recommendTimer = setTimeout(() => console.log("Đang hiển thị ảnh gợi ý"), 100);
clearTimeout(recommendTimer);
// Dừng ở đây cho đến khi qua 200ms
await delay(200);
console.log("Đã chờ 200ms"); // Đã chờ 200ms (thứ 3)
Hàm đã lên lịch vẫn có thể chạy sau khi code đã đi đến dòng cuối cùng. Để xem output của nó trong console bài tập, hãy đặt ở cuối code một dòng chờ như await delay(…), giống trong bài về Promise.
Dừng việc lặp lại — setInterval và clearInterval
Giả sử bạn muốn hiển thị số giây còn lại đến khi đợt giảm giá kết thúc, giảm dần sau mỗi giây. setTimeout chỉ gọi hàm đã lên lịch một lần, nên muốn lặp lại thì mỗi lần được gọi, hàm phải tự lên lịch lại, và điều kiện dừng cũng bị trộn lẫn vào chính hàm đó.
setInterval (hàm gọi đi gọi lại cùng một hàm sau mỗi khoảng mili giây cho trước) sẽ gọi mãi cho đến khi bạn truyền timer ID mà nó trả về vào clearInterval.
Để dòng tiếp theo chỉ chạy sau khi việc lặp kết thúc, hãy bọc toàn bộ trong một Promise và gọi resolve ngay sau clearInterval, trong cùng khối if.
// Giảm số giây còn lại đi 1 sau mỗi 100ms (trên trang thật là mỗi 1000ms)
function startCountdown(seconds) {
return new Promise((resolve) => {
let remaining = seconds; // Đặt bên ngoài hàm được gọi để giữ giá trị giữa các lần gọi
const timerId = setInterval(() => {
remaining -= 1;
console.log(`Còn ${remaining} giây`); // Còn 2 giây → Còn 1 giây → Còn 0 giây
if (remaining === 0) {
clearInterval(timerId); // Dừng ở lần gọi về 0 (lúc được gọi, timerId đã được gán)
resolve(); // Báo cho dòng await biết là đã dừng
}
}, 100);
});
}
await startCountdown(3);
console.log("Đợt giảm giá đã kết thúc"); // Đợt giảm giá đã kết thúc
- Chưa sang dòng tiếp theo cho đến khi Promise trả về được fulfilled
Đợt giảm giá đã kết thúcđược in sau resolve
let remaining = seconds— chỉ được tạo một lần ở đây, mang giá trị 3const timerId = setInterval(...)— ID cần truyền khi dừng
remaining -= 1— giảm biến bên ngoài: 2 → 1 → 0- Ở lần gọi về 0:
clearInterval(timerId)vàresolve()
Nếu chuyển let remaining = seconds vào trong hàm được gọi, mỗi lần gọi biến lại được tạo mới với giá trị 3 nên không bao giờ về 0, và cả clearInterval lẫn resolve đều không được gọi. Sau 15 giây không tiến triển, console bài tập sẽ báo lần chạy đã hết thời gian (timeout).
Lần lặp quên dừng sẽ lẫn vào các lần chạy sau
Console bài tập dùng chung một trang cho mọi lần chạy trong bài này. Nếu lần chạy kết thúc mà chưa gọi clearInterval, hàm đó sẽ tiếp tục được gọi cho đến khi bạn tải lại trang, và output của nó sẽ lẫn vào kết quả của những bài tập chạy sau có chờ bằng await, khiến cả code đúng cũng bị chấm sai. Khi gặp tình huống này, hãy tải lại trang bài viết.
Chuyển ngày tháng thành chuỗi để hiển thị — getMonth và Intl
Giả sử danh sách video cần hiển thị ngày đăng theo dạng "Thứ Tư, 2 tháng 9, 2026". Nếu dùng slice để cắt tháng ra từ chuỗi API như "2026-09-02T09:00:00", bạn sẽ nhận được chuỗi "09"; còn thứ trong tuần thì không được ghi ở đâu cả, nên chỉ dựa vào chuỗi thì không tìm ra được.
Date (object có sẵn biểu diễn ngày giờ) đọc "2026-09-02T09:00:00" thành 9 giờ theo múi giờ của thiết bị (giờ địa phương của một khu vực, xác định bằng độ lệch so với giờ chuẩn UTC), và các phương thức bắt đầu bằng get trả về tháng, thứ trong tuần... dưới dạng số.
Intl.DateTimeFormat (class định dạng ngày giờ thành chuỗi theo cách viết của từng ngôn ngữ) được tạo bằng cách truyền vào một locale như "vi-VN" (tiếng Việt).
const publishedAt = new Date("2026-09-02T09:00:00");
// Các phương thức bắt đầu bằng get trả về số. Tháng 1 là 0, còn Chủ nhật là 0
console.log(publishedAt.getMonth(), publishedAt.getDate(), publishedAt.getDay()); // 8 2 3
// Truyền ngôn ngữ và kiểu hiển thị của từng thành phần để tạo định dạng
const dateFormat = new Intl.DateTimeFormat("vi-VN", {
year: "numeric", // Năm dạng số
month: "long", // Tháng dạng "tháng 9"
day: "numeric", // Ngày dạng số
weekday: "long", // Thứ trong tuần dạng tên đầy đủ
});
console.log(dateFormat.format(publishedAt)); // Thứ Tư, 2 tháng 9, 2026
console.log(new Intl.DateTimeFormat("vi-VN", { hour: "numeric", minute: "2-digit" }).format(publishedAt)); // 9:00
// Chuỗi không có phần giờ được đọc thành 0 giờ theo UTC
const dateOnly = new Date("2026-09-02");
console.log(dateOnly.getDate()); // Ở Việt Nam là 2, ở New York là 1 (20 giờ ngày hôm trước)
Ở những múi giờ đi trước UTC như Việt Nam hay Nhật Bản, bạn vẫn nhận được 2, nên sự khác biệt này rất khó nhận ra. Khi API gửi về chuỗi chỉ có ngày, hãy nối thêm "T00:00:00" trước khi truyền vào để chuỗi được đọc thành 0 giờ theo múi giờ của thiết bị. Bảng dưới đây tóm tắt giá trị mà các phương thức bắt đầu bằng get trả về với publishedAt.
| Phương thức | Với publishedAt | Khi hiển thị |
|---|---|---|
| getFullYear() | 2026 | Dùng trực tiếp |
| getMonth() | 8 | Tháng 1 là 0, cộng 1 được 9 |
| getDate() | 2 | Tính từ 1, dùng trực tiếp |
| getDay() | 3 | Chủ nhật là 0, 3 là thứ Tư |
| getHours() | 9 | Kiểu 24 giờ, 0–23 |
Đếm số ngày đến hạn — phép trừ Date
Giả sử trang web của thư viện cần hiển thị còn bao nhiêu ngày nữa đến hạn trả sách. Nếu chỉ lấy ngày trừ ngày, sách mượn ngày 2 tháng 9 và hạn trả là ngày 3 tháng 10 sẽ cho ra 3 - 2 = 1 ngày, vì các ngày thuộc tháng 9 nằm ở giữa không được tính.
Bên trong, Date lưu một con số: số mili giây tính từ 0 giờ ngày 1 tháng 1 năm 1970 (UTC). Phép trừ giữa hai Date trả về phần chênh lệch đó, nên chia cho số mili giây của một ngày 24 * 60 * 60 * 1000 là ra số ngày.
new Date(năm, tháng, ngày) tạo ra 0 giờ của ngày đó, và ở đây tháng cũng được đếm từ 0.
// Số mili giây trong một ngày (24 giờ × 60 phút × 60 giây × 1000)
const DAY_MS = 24 * 60 * 60 * 1000;
// Ngày mượn và hạn trả. Khi tạo từ các số, tháng vẫn được đếm từ 0
const borrowedAt = new Date(2026, 8, 2); // Ngày 2 tháng 9 năm 2026
const dueDate = new Date(2026, 9, 3); // Ngày 3 tháng 10 năm 2026
// Trừ hai Date cho nhau được phần chênh lệch tính bằng mili giây
const diffMs = dueDate - borrowedAt;
console.log(Math.round(diffMs / DAY_MS)); // 31
// Truyền ngày 31, ngày mà tháng 9 không có, sẽ không báo lỗi mà nhảy sang tháng sau
const overflowDate = new Date(2026, 8, 31);
console.log(overflowDate.getMonth() + 1, overflowDate.getDate()); // 10 1
Code dùng Math.round để làm tròn vì ở những nơi áp dụng giờ mùa hè (vặn đồng hồ nhanh thêm 1 giờ trong mùa hè), có những ngày chỉ dài 23 hoặc 25 giờ, nên kết quả phép chia không phải lúc nào cũng là số nguyên mà có thể là số lẻ như 30,958. Ở những nơi không dùng giờ mùa hè như Việt Nam hay Nhật Bản, phép chia luôn ra số nguyên, nên rất dễ quên làm tròn.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2new Date("2026-09-02T09:00:00").getMonth() trả về gì?
Câu 3getDate() của Date được tạo bằng new Date(2026, 8, 31) trả về gì?