Học bằng cách đọc theo thứ tự

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ề setTimeoutsetInterval để tạo bộ hẹn giờ, cùng DateIntl.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)
Thứ tự viết khác thứ tự in ra
0msĐã tải lên xongIn ra thứ 1dòng setTimeoutkhông in gì0msHủy ảnh gợi ýĐến 100ms vẫnkhông được gọi100msĐã đóng thông báoIn ra thứ 2,trong lúc await200msĐã chờ 200msDòng sau awaitin ra thứ 3
Dòng setTimeout không in ra gì, còn lịch đã hủy thì không bao giờ được gọi. Hàm đã lên lịch chạy ở mốc 100ms, trong lúc await đang chờ.

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.

Trong một ứng dụng email, bạn cho email gửi chậm lại một chút để người dùng còn kịp hủy. delay đã được khai báo sẵn.

① Hãy lên lịch in "Đã gửi email 1" sau 150ms, chờ 50ms rồi hủy lịch đó.

② Hãy in ra "Đã hủy gửi email 1".

③ Hãy lên lịch in "Đã gửi email 2" sau 100ms, chờ 150ms rồi hủy lịch đó.

④ Hãy in ra "Không hủy được email 2".

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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
Gọi resolve thì await đi tiếp
Phía gọi — dòng await startCountdown(3)
  • 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
Bên trong startCountdown(3) — chạy 1 lần
  • let remaining = secondschỉ được tạo một lần ở đây, mang giá trị 3
  • const timerId = setInterval(...) — ID cần truyền khi dừng
Hàm được gọi mỗi 100ms — chạy 3 lần
  • remaining -= 1 — giảm biến bên ngoài: 2 → 1 → 0
  • Ở lần gọi về 0: clearInterval(timerId)resolve()
Biến remaining ở bên ngoài chỉ được tạo một lần và giảm đi sau mỗi lần gọi. resolve ở lần gọi về 0 cho phép await ở phía gọi đi tiếp.

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.

Cứ mỗi 100ms, bạn đọc số hàng tồn kho một lần và dừng sau 3 lần. Khung hàm watchStock, mảng số hàng tồn readingsdelay đã được khai báo sẵn.

① Sau mỗi 100ms, hãy đọc lần lượt các giá trị trong readings và in ra theo dạng "Lần 1: tồn kho 12".

② Hãy dừng ở lần thứ 3 và cho Promise được fulfilled.

③ Hãy chờ việc đọc hoàn tất, rồi chờ thêm 200ms để chắc rằng không có lần thứ 4.

④ Hãy in ra "Đã kiểm tra tồn kho xong".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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)
Bỏ phần giờ thì thành 0 giờ UTC
"2026-09-02T09:00:00"Đọc thành 9:00giờ địa phươngỞ New York cũng là9:00 ngày 2/9getDate() là 2như ở Việt Nam"2026-09-02"bỏ phần giờĐọc thành0:00 UTCỞ New York là20:00 ngày 1/9getDate() là 1là ngày hôm trước
Chuỗi có ghi cả giờ luôn là 9 giờ ngày 2 tháng 9, dù thiết bị ở múi giờ nào. Nếu bỏ phần giờ, chuỗi được đọc theo UTC và tùy độ lệch múi giờ có thể lùi về 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ứcVới publishedAtKhi hiển thị
getFullYear()2026Dùng trực tiếp
getMonth()8Tháng 1 là 0, cộng 1 được 9
getDate()2Tính từ 1, dùng trực tiếp
getDay()3Chủ nhật là 0, 3 là thứ Tư
getHours()9Kiểu 24 giờ, 0–23

Trên trang đặt lịch của một phòng khám nha khoa, bạn chuyển ngày giờ hẹn thành chuỗi để hiển thị. Ngày giờ hẹn reservedAt đã được khai báo sẵn.

① Hãy dùng các phương thức bắt đầu bằng get để in ngày hẹn theo dạng "5/10".

② Hãy tạo một định dạng tiếng Việt hiển thị thứ, ngày và tháng theo dạng "Thứ Hai, 5 tháng 10".

③ Hãy dùng định dạng ở ② để in ngày hẹn.

④ Hãy tạo một định dạng hiển thị giờ và phút theo dạng "14:30", rồi in giờ hẹn.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Đế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
Ba cách viết cùng nhắm tới ngày 30/9
Số ngày từnew Date(2026,8,2)đến hạn trảnew Date(2026, 8, 30)new Date(2026, 9, 30)new Date(2026, 8, 31)Tạo rangày 30/9Ngày 30/109 là tháng 10Ngày 1/10không có ngày 31/928 ngày58 ngàythừa 30 ngày29 ngàythừa 1 ngày
Cả ba đều định đặt hạn trả là ngày 30 tháng 9. Truyền 9 cho tháng hay 31 cho ngày đều không báo lỗi, chỉ có số ngày bị lệch đi.

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.

Trên trang đặt chỗ của một khu cắm trại, bạn tính số đêm lưu trú. Khung hàm toDate, countNightsDAY_MS đã được khai báo sẵn.

① Trong toDate, hãy nhận năm, tháng (đếm từ 1) và ngày, rồi trả về một Date.

② Trong countNights, hãy dùng chênh lệch giữa hai Date để trả về số đêm.

③ Hãy in số đêm từ ngày 28/9 đến ngày 2/10/2026 theo dạng "4 đêm".

④ Hãy in số đêm từ ngày 29/12/2026 đến ngày 3/1 năm sau theo cùng dạng đó.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

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?

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ì?