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

Generator trong JavaScript: vì sao duyệt lần hai lại hết dữ liệu?

Lần đầu xuất báo cáo có dữ liệu; lần sau dùng cùng biến lại nhận mảng rỗng. Nếu biến đó chứa generator object, nó lưu trạng thái duyệt thay vì hành xử như một mảng có thể đọc lại từ đầu. Cần phân biệt hàm tạo generator với iterator đã được tạo ra.

Generator trong JavaScript: vì sao duyệt lần hai lại hết dữ liệu?

Lần đầu xuất báo cáo có dữ liệu; lần sau dùng cùng biến lại nhận mảng rỗng. Nếu biến đó chứa generator object, nó lưu trạng thái duyệt thay vì hành xử như một mảng có thể đọc lại từ đầu. Cần phân biệt hàm tạo generator với iterator đã được tạo ra.

1. Một generator object tiến về phía trước

function* labels() {
  yield 'A';
  yield 'B';
}
const iterator = labels();
console.log(JSON.stringify([...iterator])); // ["A","B"]
console.log(JSON.stringify([...iterator])); // []
console.log(iterator.next().done); // true

Spread lần đầu lấy hết giá trị. Spread lần sau dùng chính iterator đã hoàn tất nên không có phần tử mới. Gán iterator sang biến khác cũng chỉ tạo thêm một tham chiếu tới cùng trạng thái, không tạo lượt duyệt độc lập.

Khi một bước xử lý nhận iterable, đừng mặc định có thể duyệt nó nhiều lần. Mảng và generator object đều dùng được với for...of nhưng có cách quản lý lượt duyệt khác nhau.

2. Gọi next để xem trước cũng tiêu thụ một giá trị

function* labels() {
  yield 'A';
  yield 'B';
}
const iterator = labels();
console.log(iterator.next().value); // A
console.log(JSON.stringify(Array.from(iterator))); // ["B"]

Đoạn log thử đã lấy A trước khi Array.from chạy. Tương tự, một bước đếm hoặc kiểm tra đầu vào có thể làm bước xuất dữ liệu nhận thiếu phần đầu nếu cùng tiêu thụ iterator. Xác định ai sở hữu việc đọc và tránh các thao tác “xem thử” không được ghi nhận.

3. Tạo iterator mới hoặc lưu kết quả có chủ đích

let runs = 0;
function* labels() {
  runs += 1;
  yield 'A';
  yield 'B';
}
console.log(JSON.stringify([...labels()])); // ["A","B"]
console.log(JSON.stringify([...labels()])); // ["A","B"]
console.log(runs); // 2
const snapshot = [...labels()];
console.log(JSON.stringify([...snapshot])); // ["A","B"]
console.log(JSON.stringify([...snapshot])); // ["A","B"]
console.log(runs); // 3

Mỗi lần gọi labels tạo một iterator mới; khi được duyệt, thân hàm chạy lại. Nếu generator thực hiện đọc nguồn bên ngoài hoặc có tác dụng phụ, chạy lại không bảo đảm cùng dữ liệu và không phải thao tác miễn phí.

Snapshot phù hợp khi đầu vào hữu hạn, đủ nhỏ và cần đọc lại cùng tập giá trị. Việc gom toàn bộ kết quả tốn bộ nhớ; không spread generator vô hạn. Mảng snapshot cũng không tự sao chép sâu object được yield, nên chỉnh sửa object vẫn cần chính sách riêng.

4. break có thể đóng generator sớm

const events = [];
function* labels() {
  try {
    yield 'A';
    yield 'B';
  } finally {
    events.push('closed');
  }
}
const iterator = labels();
for (const label of iterator) {
  console.log(label); // A
  break;
}
console.log(events.join(',')); // closed
console.log(iterator.next().done); // true

Ví dụ cho thấy thoát for...of bằng break gọi cơ chế đóng iterator và chạy finally của generator đang tạm dừng. Với generator đơn giản này, không thể dùng next để lấy B sau đó. Không suy rộng rằng mọi iterator tùy biến đều có cùng hành vi đóng: nó phụ thuộc giao thức và phương thức return của iterator.

5. Kiểm tra quyền sở hữu lượt duyệt

  • Biến chứa hàm tạo, iterable có thể duyệt lại hay iterator đang chạy?
  • Bước log, đếm hoặc kiểm tra có tiêu thụ phần tử không?
  • Hai nơi dùng dữ liệu có đang chia sẻ cùng iterator không?
  • Chạy lại nguồn có gây tác dụng phụ hoặc thay đổi kết quả không?
  • Snapshot có giới hạn kích thước và thời điểm tạo rõ ràng không?
  • Thoát sớm có chạy cleanup cần thiết không?

Tám khối ví dụ Việt–Anh đã chạy bằng Node.js, kèm kiểm tra iterator cạn, đọc một phần và đóng sớm. Không thử nguồn mạng, file hoặc async generator. Tham khảo MDN Generator và MDN for...of.

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.