Lập trình · 21/10/2026

Closure trong vòng lặp JavaScript: vì sao callback cùng đọc giá trị cuối?

Tạo ba callback cho ba dòng nhưng khi gọi, cả ba cùng đọc một chỉ mục cuối. Closure giữ quyền truy cập tới binding trong phạm vi bên ngoài, không tự chụp lại mọi giá trị khi hàm được tạo. Cần nhìn vị trí khai báo biến để biết các callback đang chia sẻ điều gì.

Closure trong vòng lặp JavaScript: vì sao callback cùng đọc giá trị cuối?

Tạo ba callback cho ba dòng nhưng khi gọi, cả ba cùng đọc một chỉ mục cuối. Closure giữ quyền truy cập tới binding trong phạm vi bên ngoài, không tự chụp lại mọi giá trị khi hàm được tạo. Cần nhìn vị trí khai báo biến để biết các callback đang chia sẻ điều gì.

Các ví dụ lưu callback trong mảng rồi gọi sau khi vòng lặp kết thúc. Cách này tái hiện vấn đề mà không cần timer, nút bấm hoặc request.

1. var dùng chung một biến đếm

const readers = [];
for (var index = 0; index < 3; index++) {
  readers.push(() => index);
}
console.log(readers.map(read => read()).join(',')); // 3,3,3

Khi gọi callback, vòng lặp đã kết thúc và index bằng 3. Cả ba callback đọc cùng binding đó. Chuyển arrow function thành function thông thường không tự sửa phạm vi của biến đếm.

2. let trong phần khai báo for tạo binding cho từng lượt

const readers = [];
for (let index = 0; index < 3; index++) {
  readers.push(() => index);
}
console.log(readers.map(read => read()).join(',')); // 0,1,2

Với let ở vị trí này, các lượt lặp có binding riêng để callback giữ lại. Đây là lý do kết quả thay đổi, không phải vì callback được gọi sớm hơn. Nếu xử lý từng bản ghi bằng for...of, khai báo const hoặc let ngay trong phần đầu vòng lặp cũng giúp diễn đạt rõ dữ liệu thuộc từng lượt.

3. let bên ngoài vòng lặp vẫn có thể bị chia sẻ

const readers = [];
let index;
for (index = 0; index < 3; index++) {
  readers.push(() => index);
}
console.log(readers.map(read => read()).join(',')); // 3,3,3

Chỉ thay từ khóa var bằng let ở một khai báo bên ngoài chưa tạo binding riêng mỗi lượt. Ví dụ tự kiểm chứng rằng vị trí khai báo là yếu tố quyết định. Khi sửa mã cũ, kiểm tra cả vị trí và vòng đời biến thay vì tìm và thay thế từ khóa hàng loạt.

4. Binding riêng không có nghĩa sao chép sâu dữ liệu

const row = { status: 'queued' };
const readCurrent = () => row.status;
const { status } = row;
const readSnapshot = () => status;
row.status = 'done';
console.log(readCurrent()); // done
console.log(readSnapshot()); // queued

readCurrent chủ đích đọc thuộc tính hiện tại của cùng object. readSnapshot đọc biến status đã nhận chuỗi 'queued' trước khi object thay đổi. Cả hai đều có thể đúng, tùy callback cần dữ liệu lúc tạo hay dữ liệu lúc chạy.

Nếu giá trị tách ra là object lồng thay vì chuỗi nguyên thủy, nó vẫn có thể dùng chung tham chiếu. const ngăn gán lại binding, không đóng băng object. Lưu ID ổn định thường phù hợp hơn lưu chỉ mục khi danh sách có thể sắp xếp lại; cách tra cứu bản ghi và xử lý bản ghi đã bị xóa cần hợp đồng riêng.

5. Kiểm thử thời điểm callback thực sự chạy

  • Gọi callback sau khi hoàn tất vòng lặp, không chỉ ngay lúc tạo.
  • Thử danh sách rỗng, một dòng và nhiều dòng.
  • Kiểm tra biến được khai báo trong đầu vòng lặp hay bên ngoài.
  • Thay đổi dữ liệu trước khi gọi để xác nhận đọc hiện tại hay snapshot.
  • Với giao diện thực tế, thử thêm trường hợp đổi thứ tự hoặc xóa bản ghi.

Tám khối ví dụ Việt–Anh đã chạy bằng Node.js, kèm kiểm tra số lượt lặp khác nhau và giá trị sau thay đổi. Chưa kiểm thử timer hoặc component trình duyệt. Tham khảo MDN Closures và MDN for.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.