Một câu hỏi ngắn trong buổi phỏng vấn: gọi test ba lần trên cùng regex và cùng chuỗi, có chắc nhận cùng kết quả không? Nếu regex mang cờ g, cần biết trạng thái lastIndex trước mỗi lần gọi. Chỉ đọc pattern và chuỗi đầu vào là chưa đủ để dự đoán toàn bộ hành vi.
Bài đối chiếu tài liệu ngày 08/11/2026 và chạy ví dụ trên Node.js 24.18.1. Đây là kiểm tra ngữ nghĩa JavaScript với dữ liệu nhỏ, không phải benchmark regex, đánh giá hiệu năng ứng dụng hoặc kiểm thử validator trên browser.
Dự đoán cả kết quả lẫn con trỏ
const codePattern = /^DEV-\d+$/g;
for (let i = 0; i < 3; i += 1) {
const matched = codePattern.test('DEV-42');
console.log(matched, codePattern.lastIndex);
}
Ba dòng kết quả là true 6, false 0, true 6. Chuỗi không đổi, nhưng cùng một instance regex được tái sử dụng. Cờ g phục vụ tìm kiếm có trạng thái; test không tự biến nó thành một phép kiểm tra độc lập ở mọi lần gọi.
Theo RegExp.test, một lần khớp thành công với cờ g làm tiến lastIndex. Lần gọi tiếp tục từ vị trí đó; khi không khớp, con trỏ được đặt lại 0. Trong ví dụ, lần thứ hai bắt đầu sau đoạn đã khớp và không thể thỏa toàn bộ pattern có dấu neo đầu chuỗi.
Lỗi thực tế khi dùng làm predicate cho filter
const codes = ['DEV-1', 'DEV-2', 'DEV-3'];
const shared = /^DEV-\d+$/g;
const accepted = codes.filter(code => shared.test(code));
console.log(JSON.stringify(accepted));
Kết quả là ["DEV-1","DEV-3"]. DEV-2 bị loại dù có cùng cấu trúc với hai mã còn lại. Trạng thái của shared đi qua các callback, nên mỗi phần tử không được kiểm tra với một con trỏ mới bắt đầu từ 0.
Trong phỏng vấn, hãy giải thích đường đi: lần đầu khớp làm lastIndex thành 5; lần thứ hai thất bại và đưa nó về 0; lần thứ ba lại khớp. Cách giải thích theo từng trạng thái có thể kiểm chứng hơn việc chỉ nói “regex global có bug”. Đây là hành vi API đã định nghĩa, còn lỗi nằm ở cách chọn regex cho nhiệm vụ.
Validator này không cần tìm kiếm toàn cục
Nếu yêu cầu chỉ là toàn bộ chuỗi có đúng dạng DEV rồi dấu gạch ngang và ít nhất một chữ số ASCII, bỏ cờ g:
const validatorPattern = /^DEV-\d+$/;
function isDeveloperCode(value) {
return typeof value === 'string' && validatorPattern.test(value);
}
const inputs = ['DEV-1', 'DEV-2', 'DEV-3', 'DEV-', 'XDEV-4', null];
console.log(JSON.stringify(inputs.filter(isDeveloperCode)));
console.log(isDeveloperCode('DEV-42'), isDeveloperCode('DEV-42'));
Danh sách còn ba mã hợp lệ và hai lần kiểm tra cuối đều true. Type guard là hợp đồng của ví dụ: null và số không được tự ép thành chuỗi. Pattern không làm nhiệm vụ xác nhận một mã tồn tại trong database hoặc người dùng có quyền xem nó.
Bạn cũng có thể chủ động đặt lastIndex về 0 trước mỗi lần test một regex có trạng thái. Tuy nhiên, với validator này, bỏ cờ g làm ý định rõ hơn và tránh phải nhớ thao tác reset ở mọi chỗ gọi. Đừng bỏ g khỏi toàn bộ codebase nếu các chỗ khác thực sự dùng nó để duyệt nhiều kết quả.
Cờ y cũng cần chú ý trạng thái
Tài liệu lastIndex bàn cả regex sticky với cờ y. Sticky yêu cầu khớp ngay tại vị trí con trỏ, hữu ích trong một số bộ phân tích chuỗi. Không nên thay g bằng y rồi coi như đã tạo validator không có trạng thái.
Khi review một helper nhận regex từ bên ngoài, kiểm tra cả flags lẫn quyền sở hữu instance. Một pattern được chia sẻ giữa nhiều lần gọi có thể mang theo trạng thái từ chỗ sử dụng trước. Log thêm lastIndex ở trước và sau thao tác trong dữ liệu thử nghiệm để kiểm chứng giả thuyết.
Cách trả lời và bài tập tiếp theo
Một câu trả lời tốt gồm ba phần: dự đoán đúng kết quả, chỉ ra trạng thái làm kết quả đổi và sửa theo yêu cầu cụ thể. Hãy thử đổi thứ tự mảng, thêm một mã không hợp lệ ở đầu, rồi dự đoán lại trước khi chạy. Mục tiêu là hiểu đường đi của con trỏ, không học thuộc hiện tượng bỏ xen kẽ như một quy luật cho mọi regex.
Với kiểm tra định dạng độc lập từng chuỗi, chọn pattern không giữ con trỏ tìm kiếm khi không cần thiết. Với tìm kiếm nhiều lần trên cùng đầu vào, quản lý lastIndex như một phần trạng thái của thuật toán.

