Tại sao Regex không khớp? Tìm hiểu cờ Flags, chế độ Multiline và ký tự Escape
Mục lục nội dung
1. Bản chất của các cờ Flags trong biểu thức chính quy
Biểu thức chính quy (Regular Expressions – RegEx) là công cụ cực kỳ mạnh mẽ để tìm kiếm và trích xuất chuỗi. Tuy nhiên, ngay cả một biểu thức được viết rất cẩn thận cũng có thể trả về kết quả rỗng nếu lập trình viên không thiết lập đúng các cờ điều khiển (Flags):
| Cờ Flag | Tên đầy đủ | Tác động đến hành vi khớp mẫu |
|---|---|---|
g |
Global Search | Tìm kiếm tất cả các kết quả phù hợp trên toàn chuỗi thay vì dừng lại ngay sau kết quả đầu tiên. Duy trì thuộc tính trạng thái lastIndex. |
m |
Multiline Mode | Thay đổi hành vi của các ký tự neo ^ và $: khớp ở đầu và cuối của từng dòng riêng biệt thay vì chỉ khớp ở đầu và cuối của toàn bộ văn bản. |
s |
dotAll Mode | Cho phép ký tự chấm (.) khớp với cả ký tự ngắt dòng (\n và \r). |
i |
Ignore Case | So khớp không phân biệt chữ hoa và chữ thường (ví dụ: /a/i khớp cả 'a' và 'A'). |
u |
Unicode Support | Xử lý đúng các ký tự Unicode 4-byte (surrogate pairs) và thuộc tính ký tự Unicode \p{...}. |
2. Bẫy trạng thái lastIndex khi dùng cờ g trong vòng lặp
Một lỗi phổ biến khiến nhiều lập trình viên JavaScript bối rối là việc hàm RegExp.test() luân phiên trả về true rồi lại false khi kiểm tra các chuỗi giống hệt nhau:
const regex = /abc/g;
console.log(regex.test('abc')); // true (lastIndex chuyển thành 3)
console.log(regex.test('abc')); // false (tìm từ vị trí 3, thất bại, lastIndex reset về 0)
console.log(regex.test('abc')); // true (lại thành true!)Khi bật cờ g, đối tượng RegExp lưu giữ trạng thái vị trí tìm kiếm tiếp theo trong thuộc tính lastIndex. Để kiểm tra tính hợp lệ đơn thuần, bạn nên bỏ cờ g hoặc gán lại regex.lastIndex = 0; trước mỗi lần kiểm tra.
3. Chế độ Tham lam (Greedy) vs Lười biếng (Lazy)
Mặc định, các toán tử lặp lượng từ (*, +, {n,m}) hoạt động ở chế độ tham lam (greedy) – tức là cố gắng khớp càng nhiều ký tự càng tốt. Điều này thường dẫn đến việc bắt nhầm toàn bộ khối văn bản lớn thay vì từng thẻ riêng lẻ:
// Chuỗi nguồn: "<b>Một</b> và <b>Hai</b>"
// ❌ Greedy: <.*> khớp toàn bộ từ <b> đầu tiên đến </b> cuối cùng
// Kết quả: "<b>Một</b> và <b>Hai</b>"
// ✅ Lazy: <.*?> dừng lại ngay khi gặp dấu > đầu tiên
// Kết quả: 4 kết quả riêng biệt: "<b>", "</b>", "<b>", "</b>"