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

JavaScript RegExp.test: vì sao cùng đầu vào lại lúc true, lúc false?

Cùng một chuỗi được kiểm tra hai lần nhưng kết quả lần đầu là true, lần sau là false. Trước khi nghi ngờ dữ liệu hay bộ nhớ đệm, hãy xem regex có cờ g hoặc y hay không. Với hai cờ này, RegExp mang trạng thái qua thuộc tính lastIndex.

JavaScript RegExp.test: vì sao cùng đầu vào lại lúc true, lúc false?

Cùng một chuỗi được kiểm tra hai lần nhưng kết quả lần đầu là true, lần sau là false. Trước khi nghi ngờ dữ liệu hay bộ nhớ đệm, hãy xem regex có cờ g hoặc y hay không. Với hai cờ này, RegExp mang trạng thái qua thuộc tính lastIndex.

test trả boolean nhưng có thể đồng thời cập nhật vị trí tìm kiếm. Vì vậy một regex dùng để quét văn bản và một regex dùng làm phép kiểm tra độc lập cần được thiết kế theo mục đích khác nhau.

1. Tái hiện kết quả thay đổi trên cùng đầu vào

const pattern = /cat/g;

console.log(pattern.test('cat')); // true
console.log(pattern.lastIndex);   // 3
console.log(pattern.test('cat')); // false
console.log(pattern.lastIndex);   // 0
console.log(pattern.test('cat')); // true

Lần đầu tìm thấy cat và đặt lastIndex ở cuối kết quả khớp. Lần sau bắt đầu tìm từ vị trí đó nên không tìm được; khi thất bại, vị trí trở về 0. Lần thứ ba lại tìm thấy từ đầu.

Cờ g không chỉ có nghĩa “kiểm tra toàn bộ chuỗi”. Nó cho phép tìm các lần khớp tiếp theo bằng trạng thái dùng chung. Đặc biệt, lastIndex vẫn được dùng khi lần gọi sau nhận một chuỗi khác.

2. Lỗi có thể xuất hiện trong filter

const pattern = /cat/g;
const names = ['cat', 'cat', 'cat'];
const kept = names.filter(name => pattern.test(name));

console.log(kept.length); // 2
console.log(names.length); // 3

Ba phần tử bằng nhau nhưng phần tử giữa bị bỏ do trạng thái từ lần kiểm tra trước. Bản thân filter không tự tạo regex mới hoặc đặt lại lastIndex cho callback.

Khi điều tra lỗi, hãy ghi lại flags và lastIndex trước/sau mỗi lần gọi trên một mẫu nhỏ. Chỉ log boolean sẽ bỏ mất thông tin giúp giải thích vì sao kết quả thay đổi.

3. Phép kiểm tra độc lập thường không cần g hoặc y

const pattern = /cat/;
const names = ['cat', 'cat', 'cat'];

console.log(names.filter(name => pattern.test(name)).length); // 3
console.log(pattern.test('cat')); // true
console.log(pattern.test('cat')); // true

Nếu chỉ hỏi chuỗi có chứa một mẫu hay không, bỏ cờ trạng thái thường diễn đạt đúng mục đích hơn. Vẫn giữ các cờ khác nếu yêu cầu cần, chẳng hạn i cho không phân biệt hoa/thường; không sao chép cờ g từ một phép replace hoặc quét match sang validator một cách máy móc.

test chỉ tìm một lần khớp, không tự xác nhận toàn bộ đầu vào hợp lệ. Ví dụ /cat/ cũng khớp trong concatenate. Với kiểm tra định dạng trường nhập, cần quy tắc cho toàn chuỗi, độ dài và ký tự được phép theo sản phẩm.

4. Sticky yêu cầu khớp ngay tại lastIndex

const sticky = /cat/y;
sticky.lastIndex = 1;
console.log(sticky.test(' cat')); // true
console.log(sticky.lastIndex);    // 4

sticky.lastIndex = 0;
console.log(sticky.test(' cat')); // false
console.log(sticky.lastIndex);    // 0

Khác với tìm kiếm tiến về phía sau của regex global, sticky yêu cầu mẫu khớp đúng vị trí đang chỉ định. Nó có ích cho quá trình đọc token có quản lý vị trí, nhưng không phù hợp nếu người viết tưởng y chỉ là một tùy chọn vô hại cho phép tìm “có ở đâu đó”.

Nếu cố ý dùng regex có trạng thái, hãy để vòng quét sở hữu instance đó và quản lý vị trí rõ ràng. Không chia sẻ cùng instance giữa nhiều luồng xử lý logic mà mỗi bên đều tưởng đang bắt đầu từ đầu.

5. Kiểm tra kiểu đầu vào trước test

const asciiDigit = /[0-9]/;

function containsAsciiDigit(value) {
  if (typeof value !== 'string') {
    throw new TypeError('Expected text');
  }
  return asciiDigit.test(value);
}

console.log(containsAsciiDigit('A2')); // true
console.log(containsAsciiDigit('AB')); // false
console.log(containsAsciiDigit('A2')); // true

Hàm này chỉ hỏi có chữ số ASCII hay không. Nó không kiểm tra mật khẩu, mã sản phẩm hoặc mọi chữ số Unicode. Không trim và không tự đổi số sang chuỗi; những quyết định đó phải được ghi trong contract nếu có.

RegExp.test có thể tự chuyển đối số thành chuỗi. Vì thế dữ liệu thiếu không nhất thiết tạo lỗi ngay như mong muốn. Một lớp kiểm tra kiểu rõ ràng giúp phát hiện lỗi bên gọi trước khi chạy mẫu.

6. Bộ ca kiểm thử nên chạy lặp lại

  • Gọi nhiều lần với cùng chuỗi, không tạo lại regex giữa các lần.
  • Đổi thứ tự đầu vào để phát hiện kết quả phụ thuộc trạng thái.
  • Thử danh sách có nhiều phần tử giống nhau.
  • Kiểm tra lastIndex sau cả thành công và thất bại nếu dùng g/y có chủ đích.
  • Thử chuỗi rỗng, dữ liệu sai kiểu và ký tự ngoài miền được phép.
  • Với vòng lặp exec và mẫu có thể khớp chuỗi rỗng, thiết kế bước tiến vị trí riêng để tránh lặp vô hạn.

Các đoạn code đã chạy bằng Node.js local. Chưa kiểm thử form trình duyệt hoặc đo hiệu năng regex trên dữ liệu lớn. Tài liệu đối chiếu: RegExp.prototype.test và lastIndex.

Khi một phép kiểm tra cần độc lập, hãy loại bỏ trạng thái không cần thiết. Khi cần quét có trạng thái, đặt quyền quản lý lastIndex vào đúng nơi điều khiển quá trình đó.

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.