Menu

Regex trong JavaScript: test, match, replace và nhóm bắt

Biểu thức chính quy hoạt động thế nào trong JavaScript: tạo mẫu, các phương thức chính (test, match, replace), cờ và nhóm bắt, kèm ví dụ thực tế.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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:

  • \d chữ số, \w ký tự từ (chữ cái, chữ số, dấu gạch dưới), \s khoả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 g với replace hoặc matchAll. Chỉ chỗ khớp đầu tiên được thay, hoặc gặp TypeError.
  • lastIndex lưu trạng thái trên regex global. Regex có cờ g hoặc y nhớ vị trí dừng lại giữa các lần gọi test/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ùng matchAll.
  • 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.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu