Regex là một mẫu để khớp văn bản
Biểu thức chính quy (regular expression) mô tả hình dạng của chuỗi: "bốn chữ số", "một từ theo sau là dấu phẩy", "bất cứ thứ gì trông giống email". JavaScript cho bạn hai cách để tạo một biểu thức:
Dạng literal, với dấu gạch chéo bao quanh mẫu và cờ đặt sau dấu gạch chéo đóng, là dạng bạn dùng nhiều nhất. Hãy dùng new RegExp(...) khi bản thân mẫu là động, chẳng hạn dựng từ dữ liệu người dùng nhập hoặc từ một biến.
Chữ i ở cuối là một cờ (flag). i nghĩa là không phân biệt chữ hoa chữ thường. Lát nữa sẽ nói thêm về cờ.
test: có khớp không?
Câu hỏi nhanh nhất bạn có thể hỏi một regex là "chuỗi này có chứa chỗ khớp không?". Đó là test:
\d nghĩa là "một chữ số bất kỳ". test chỉ trả về true hoặc false. Nếu bạn chỉ cần câu trả lời có hoặc không, như kiểm tra một trường nhập liệu hay lọc một mảng, test là công cụ phù hợp.
match: lấy phần văn bản khớp ra
Khi bạn muốn lấy chính phần văn bản khớp, hãy dùng phương thức match của chuỗi:
Không có cờ g, match trả về một mảng chứa chỗ khớp đầu tiên cùng siêu dữ liệu (index, input). Có g, nó trả về mọi chỗ khớp dưới dạng một mảng chuỗi thông thường. Nếu không có gì khớp, bạn nhận được null chứ không phải mảng rỗng, nên cần kiểm tra trước:
Cờ thay đổi cách mẫu hoạt động
Cờ đặt sau dấu gạch chéo đóng và điều chỉnh cách so khớp. Những cờ bạn hay dùng nhất:
g: global, tìm mọi chỗ khớp thay vì chỉ chỗ đầu tiên.i: không phân biệt chữ hoa chữ thường.m: multiline, để^và$khớp đầu và cuối mỗi dòng, không chỉ đầu và cuối chuỗi.s: dotall, để.khớp cả ký tự xuống dòng.u: hỗ trợ unicode, cần cho nhiều mẫu có emoji và ký tự ngoài ASCII.
Không có m, ^ chỉ neo vào đầu chuỗi. Có m, nó neo vào đầu mỗi dòng, nên cả Roses lẫn Violets đều được lấy ra.
Lớp ký tự và lượng từ
Những viên gạch tạo nên phần lớn các mẫu:
\dchữ số,\wký tự từ (chữ cái, chữ số, dấu gạch dưới),\skhoảng trắng.[abc]một trong a, b hoặc c.[^abc]bất cứ ký tự nào ngoại trừ chúng.[a-z]một khoảng..bất kỳ ký tự nào trừ ký tự xuống dòng.*không hoặc nhiều lần,+một hoặc nhiều lần,?không hoặc một lần.{3}đúng ba lần,{2,5}từ hai đến năm lần,{2,}từ hai lần trở lên.^đầu,$cuối.
Ghép lại với nhau:
\b là ranh giới từ (word boundary), đường vô hình giữa một ký tự từ và một ký tự không phải từ. Rất hữu ích khi bạn muốn khớp "nguyên một từ". Để xem một mẫu khớp những gì trước khi dùng nó trong code, hãy thử trong công cụ kiểm tra regex.
Nhóm bắt: ghi nhớ từng phần của chỗ khớp
Cặp ngoặc tròn tạo ra một nhóm bắt lấy bất cứ thứ gì nó khớp. exec và match trả về các phần bắt được này cùng với chỗ khớp tổng thể:
Chỉ số 0 là toàn bộ chỗ khớp, và mỗi nhóm có ô riêng ngay sau đó. Đếm nhóm theo vị trí trở nên rối khi có hơn hai nhóm, nên hãy đặt tên cho chúng:
Nhóm có tên dễ đọc hơn ở chỗ dùng và vẫn đúng khi bạn sắp xếp lại mẫu.
replace: viết lại phần văn bản khớp
replace nhận một mẫu và một giá trị thay thế. Giá trị thay thế có thể là chuỗi hoặc hàm:
Không có g, chỉ chỗ khớp đầu tiên được thay. Lỗi hay gặp là quên cờ rồi thắc mắc vì sao email thứ hai vẫn sai.
Chuỗi thay thế hỗ trợ tham chiếu ngược. $1, $2... trỏ tới các nhóm bắt; $<name> trỏ tới nhóm có tên:
Với mọi thứ phức tạp hơn một phép thay thế thẳng, hãy truyền vào một hàm. Hàm này nhận chỗ khớp và các phần bắt được làm đối số:
Dấu gạch dưới là toàn bộ chỗ khớp (ta không cần nó); n là nhóm bắt đầu tiên. Cặp regex và hàm thay thế này xử lý được phần lớn việc biến đổi văn bản ngoài thực tế.
matchAll: mọi chỗ khớp kèm các nhóm
String.prototype.matchAll trả về một iterator gồm mọi chỗ khớp kèm theo các nhóm bắt, điều mà match thông thường với cờ g không làm được:
matchAll bắt buộc phải có cờ g. Thiếu nó, bạn sẽ gặp TypeError. Trải nó ra thành mảng ([...text.matchAll(email)]) nếu bạn cần truy cập ngẫu nhiên thay vì duyệt tuần tự.
Thoát ký tự đặc biệt
Các ký tự như . * + ? ( ) [ ] { } | \ ^ $ có ý nghĩa đặc biệt trong regex. Để khớp đúng chúng theo nghĩa đen, hãy thoát (escape) bằng dấu gạch chéo ngược:
Phiên bản không thoát khớp cả examplexcom vì . nghĩa là "bất kỳ ký tự nào". Loại bug này rất phổ biến và không gây ra lỗi nào. Nếu một regex khớp quá nhiều, hãy tìm dấu . chưa thoát trước tiên.
Khi dựng mẫu từ dữ liệu người dùng nhập, bạn bắt buộc phải thoát nó, nếu không người dùng có thể chèn cú pháp regex vào:
$& trong chuỗi thay thế là cách viết tắt của "toàn bộ chỗ khớp".
Lookahead và lookbehind
Đôi khi bạn chỉ muốn khớp văn bản khi theo sau (hoặc đứng trước) nó là một thứ gì đó, mà không đưa thứ đó vào chỗ khớp. Đó là việc của lookaround:
(?= ...)lookahead khẳng định: "theo sau là".(?<= ...)lookbehind khẳng định: "đứng trước là".(?! ...)và(?<! ...)là các phiên bản phủ định.
Lookaround không tiêu thụ ký tự, nên phần bạn "nhìn vào" vẫn còn nguyên cho phần tiếp theo của mẫu.
Vài lời về kiểm tra email
Bạn sẽ gặp yêu cầu này rất nhiều: "cho tôi một regex kiểm tra email". Câu trả lời thật lòng là: đừng. Ngữ pháp email thực tế rất rắc rối, và regex nào đủ ngắn để đọc được thì đều sai theo hướng này hay hướng khác. Với việc kiểm tra form, một mẫu thực dụng là đủ:
Đọc nó như sau: "vài ký tự không phải khoảng trắng và không phải @, một dấu @, thêm vài ký tự như vậy, một dấu chấm, rồi thêm vài ký tự như vậy nữa". Mẫu này bắt được lỗi gõ rõ ràng mà không giả vờ tuân thủ RFC 5322. Để xác minh thật sự, hãy gửi email xác nhận.
Những lỗi thường gặp
Vài cái bẫy đáng ghi nhớ:
- Quên
gvớireplacehoặcmatchAll. Chỉ chỗ khớp đầu tiên được thay, hoặc gặpTypeError. lastIndexlưu trạng thái trên regex global. Regex có cờghoặcynhớ vị trí dừng lại giữa các lần gọitest/exec. Đừng dùng lại một regex cho các chuỗi không liên quan: hãy tạo regex mới, hoặc dùngmatchAll.- Dấu chấm và dấu gạch chéo chưa thoát trong mẫu động. Luôn thoát dữ liệu người dùng nhập trước khi đưa vào
new RegExp(...). - Quay lui thảm họa (catastrophic backtracking). Lượng từ lồng nhau như
(a+)+gặp đầu vào bất lợi có thể làm treo cả tab. Nếu một regex có vẻ chậm, hãy đơn giản hóa nó.
Tiếp theo: ngày và giờ
Regex xử lý hình dạng của văn bản; dữ liệu thực tế còn đi kèm mốc thời gian cần phân tích, định dạng và tính toán. Trang tiếp theo nói về Date, Intl.DateTimeFormat và mô hình tư duy giúp bạn tránh xa bug múi giờ.
Câu hỏi thường gặp
Làm sao tạo regex trong JavaScript?
Có hai cách. Dạng literal dùng dấu gạch chéo: /hello/i. Hàm khởi tạo nhận một chuỗi: new RegExp('hello', 'i'). Dùng literal khi mẫu cố định, và dùng hàm khởi tạo khi bạn cần dựng mẫu từ một biến lúc chạy.
test, match và exec trong regex JavaScript khác nhau thế nào?
regex.test(str) trả về một boolean, nhanh nhất khi bạn chỉ quan tâm có khớp hay không. str.match(regex) trả về một mảng các kết quả khớp (hoặc null). regex.exec(str) trả về từng kết quả khớp một, kèm các nhóm bắt, và với cờ g thì nó ghi nhớ vị trí giữa các lần gọi qua lastIndex.
Làm sao thay thế mọi chỗ khớp bằng regex trong JavaScript?
Dùng cờ g: str.replace(/foo/g, 'bar'). Không có g thì chỉ chỗ khớp đầu tiên được thay. Bạn cũng có thể gọi str.replaceAll(/foo/g, 'bar'), nhưng replaceAll với đối số là regex bắt buộc phải có cờ g, nếu không sẽ ném lỗi.
Nhóm bắt trong regex JavaScript là gì?
Cặp ngoặc tròn trong mẫu tạo ra nhóm bắt (capture group), ghi nhớ phần văn bản mà nó khớp. /(\d{4})-(\d{2})/.exec('2024-11') trả về một mảng mà chỉ số 1 là '2024' và chỉ số 2 là '11'. Đặt tên cho nhóm bằng (?<year>\d{4}) và truy cập qua match.groups.year.