This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Biểu thức chính quy (Regular Expressions — viết tắt là Regex hoặc RegExp) là một ngôn ngữ mini chuyên để mô tả khuôn mẫu (pattern) của chuỗi ký tự. Với Regex, bạn có thể tìm kiếm, kiểm tra tính hợp lệ, trích xuất và thay thế văn bản chỉ bằng vài dòng lệnh thay vì hàng chục vòng lặp thủ công. Đây là một trong những kỹ năng "lợi hại" nhất mà mọi lập trình viên JavaScript nên làm chủ. Bài 7 này sẽ dẫn dắt bạn đi từ con số 0 đến trình độ chuyên nghiệp, kèm theo một bộ Regex Tester trực tiếp để bạn thử nghiệm ngay trên trình duyệt.

1. Regex là gì & Cách tạo Regex

Trong JavaScript, Regex là một đối tượng (object) thuộc lớp RegExp. Có hai cách để khởi tạo một biểu thức chính quy, và việc chọn đúng cách sẽ giúp code của bạn vừa nhanh vừa dễ bảo trì.

Cách 1: Literal (cú pháp chữ /.../)

Bạn viết mẫu nằm giữa hai dấu gạch chéo /pattern/flags. Đây là cách phổ biến nhất vì ngắn gọn, dễ đọc và được biên dịch một lần duy nhất khi script được nạp (tối ưu hiệu năng). Hãy dùng literal khi mẫu của bạn cố định, biết trước lúc viết code.

Cách 2: Constructor new RegExp()

Bạn truyền mẫu dưới dạng chuỗi (string) vào hàm dựng new RegExp(pattern, flags). Vì mẫu là chuỗi nên mọi dấu gạch chéo ngược \ phải được nhân đôi (ví dụ "\\d" để biểu diễn \d). Hãy dùng cách này khi mẫu được tạo động lúc chạy (ghép từ biến, nhập từ người dùng).

Literal so với Constructor
// Cách 1 — Literal: mẫu cố định, biên dịch một lần
const reLiteral = /\d{3}-\d{4}/;
console.log(reLiteral.test("123-4567")); // true

// Cách 2 — Constructor: mẫu động, ghép từ biến
const tu = "javascript";
const reDong = new RegExp(`\\b${tu}\\b`, "gi"); // chú ý nhân đôi dấu \
console.log("Tôi yêu JavaScript".match(reDong)); // ["JavaScript"]

Các cờ (flags) điều chỉnh hành vi

Cờ được đặt sau dấu gạch chéo đóng và quyết định cách Regex hoạt động:

  • g (global): tìm tất cả kết quả khớp, không dừng ở lần đầu tiên.
  • i (ignoreCase): không phân biệt chữ hoa/thường.
  • m (multiline): ^$ khớp đầu/cuối từng dòng thay vì cả chuỗi.
  • s (dotAll): cho phép dấu . khớp luôn cả ký tự xuống dòng \n.
  • u (unicode): xử lý đúng các ký tự Unicode/emoji và cú pháp \u{...}.
  • y (sticky): chỉ khớp đúng vị trí lastIndex, không "nhảy" tới phía trước.
Tác dụng của các cờ
const text = "Cat cat CAT";
console.log(text.match(/cat/g));   // ["cat"]  — chỉ khớp đúng chữ thường
console.log(text.match(/cat/gi));  // ["Cat", "cat", "CAT"] — bỏ qua hoa/thường

// Cờ s cho phép dấu . vượt qua xuống dòng
console.log(/a.b/.test("a\nb"));  // false
console.log(/a.b/s.test("a\nb")); // true

Trước khi đi sâu hơn, hãy làm quen với công cụ thử nghiệm bên dưới. Hãy nhập mẫu vào ô Pattern, chỉnh cờ và gõ văn bản kiểm tra — kết quả khớp sẽ hiện ra ngay lập tức. Bạn nên quay lại đây để thử mọi ví dụ trong bài viết.

🔍 Regex Tester trực tiếp

2. Character Classes & Metacharacters

Metacharacters (siêu ký tự) là những ký tự mang ý nghĩa đặc biệt thay vì khớp đúng bản thân chúng. Chúng là "bảng chữ cái" của Regex. Character classes (lớp ký tự) cho phép bạn định nghĩa một tập hợp các ký tự được phép xuất hiện tại một vị trí.

Các siêu ký tự thông dụng nhất bao gồm: dấu . khớp một ký tự bất kỳ (trừ xuống dòng); \d khớp một chữ số (0-9); \w khớp một ký tự "từ" (chữ cái, chữ số, dấu gạch dưới); \s khớp ký tự khoảng trắng (space, tab, xuống dòng). Mỗi siêu ký tự đều có phiên bản phủ định viết hoa: \D, \W, \S nghĩa là "không phải" loại tương ứng.

Hai siêu ký tự neo vị trí (anchors) rất quan trọng: ^ neo vào đầu chuỗi, còn $ neo vào cuối chuỗi. Chúng không khớp ký tự nào cả mà chỉ khẳng định một vị trí. Tương tự, \branh giới từ (word boundary) — vị trí chuyển tiếp giữa ký tự "từ" và "không phải từ", cực kỳ hữu ích để khớp trọn một từ.

Bạn tự tạo lớp ký tự bằng cặp ngoặc vuông []: ví dụ [aeiou] khớp một nguyên âm bất kỳ. Dùng dấu gạch ngang để biểu diễn khoảng (range) như [a-z], [0-9], [A-Za-z0-9]. Đặt dấu mũ ^ ngay đầu ngoặc để phủ định: [^0-9] khớp mọi ký tự không phải chữ số.

Escaping (thoát ký tự): khi muốn khớp đúng một siêu ký tự theo nghĩa đen (ví dụ dấu chấm thật, dấu cộng thật), bạn phải đặt dấu \ phía trước: \., \+, \?, \(. Quên escape là lỗi phổ biến nhất với người mới.

Character classes cơ bản
console.log("Phòng 404".match(/\d+/));        // ["404"]
console.log("a1b2c3".match(/[a-z]/g));         // ["a", "b", "c"]
console.log("Giá: 9.99$".match(/\d+\.\d+/));   // ["9.99"] — dấu . được escape
console.log("xin chào".match(/[^aeiou ]/g));   // các phụ âm: ["x","n","c","h"]
Anchors và word boundary
console.log(/^Xin/.test("Xin chào"));   // true  — bắt đầu bằng "Xin"
console.log(/chào$/.test("Xin chào"));   // true  — kết thúc bằng "chào"
console.log("category cat".match(/\bcat\b/g)); // ["cat"] — chỉ từ "cat" đứng riêng

Bảng tra cứu nhanh các siêu ký tự quan trọng:

Ký hiệu Ý nghĩa Ví dụ khớp
. Một ký tự bất kỳ (trừ xuống dòng) a.c → "abc", "a c"
\d / \D Chữ số / không phải chữ số \d\d → "42"
\w / \W Ký tự từ / không phải ký tự từ \w+ → "user_1"
\s / \S Khoảng trắng / không khoảng trắng a\sb → "a b"
\b Ranh giới từ \bvà\b
^ $ Đầu / cuối chuỗi (hoặc dòng với cờ m) ^abc$
[abc] Một trong các ký tự liệt kê "a", "b", "c"
[^abc] Bất kỳ ký tự nào ngoài a, b, c "d", "x"

3. Quantifiers & Greedy so với Lazy

Quantifiers (lượng từ) cho biết một thành phần được lặp lại bao nhiêu lần. Đây là nơi Regex bắt đầu trở nên mạnh mẽ thực sự.

  • * — lặp 0 hoặc nhiều lần.
  • + — lặp 1 hoặc nhiều lần (ít nhất một).
  • ? — lặp 0 hoặc 1 lần (tùy chọn).
  • {n} — lặp đúng n lần.
  • {n,} — lặp ít nhất n lần.
  • {n,m} — lặp từ n đến m lần.
Các lượng từ cơ bản
console.log("color colour".match(/colou?r/g)); // ["color","colour"] — u tùy chọn
console.log("aaa".match(/a+/));                 // ["aaa"]
console.log("2026".match(/\d{4}/));             // ["2026"] — đúng 4 chữ số
console.log("ab abc abcd".match(/abc?d?/g));    // ["ab","abc","abcd"]

Greedy (tham lam) so với Lazy (lười biếng)

Mặc định, các lượng từ là greedy: chúng cố khớp càng nhiều ký tự càng tốt, rồi mới "nhả" bớt ra (backtrack) nếu cần để toàn bộ mẫu khớp được. Thêm dấu ? ngay sau lượng từ sẽ biến nó thành lazy: khớp càng ít càng tốt. Sự khác biệt này cực kỳ quan trọng khi xử lý văn bản có thẻ hoặc dấu ngoặc.

Greedy so với Lazy
const html = "<b>đậm</b> và <i>nghiêng</i>";

// Greedy: <.+> ăn từ dấu < đầu tiên tới dấu > CUỐI cùng
console.log(html.match(/<.+>/)[0]);
// "<b>đậm</b> và <i>nghiêng</i>"

// Lazy: <.+?> dừng ngay tại dấu > đầu tiên
console.log(html.match(/<.+?>/g));
// ["<b>", "</b>", "<i>", "</i>"]
[!WARNING] ReDoS & thảm họa backtracking: Khi viết những mẫu có lượng từ lồng nhau như (a+)+$ hoặc (\d+)*$, công cụ Regex có thể phải thử số tổ hợp tăng theo cấp số nhân khi gặp chuỗi không khớp. Đây gọi là "catastrophic backtracking" và có thể khiến trình duyệt treo cứng. Kẻ tấn công lợi dụng điều này để gây ReDoS (Regular expression Denial of Service). Hãy tránh lượng từ lồng nhau, ưu tiên lớp ký tự cụ thể và đặt giới hạn {n,m} rõ ràng.
Mẫu nguy hiểm so với mẫu an toàn
// ❌ NGUY HIỂM: lượng từ lồng nhau → catastrophic backtracking
const xau = /^(a+)+$/;

// ✅ AN TOÀN: diễn đạt cùng ý đồ mà không lồng lượng từ
const tot = /^a+$/;
console.log(tot.test("aaaa")); // true

4. Groups & References

Groups (nhóm) dùng cặp ngoặc tròn để gom nhiều thành phần lại với nhau. Chúng phục vụ ba mục đích: áp lượng từ cho cả cụm, trích xuất phần con của kết quả khớp, và tham chiếu lại sau này.

Capturing group — nhóm bắt giữ ( )

Mỗi cặp ( ) tạo ra một nhóm được đánh số từ 1, và phần văn bản nó khớp được lưu lại để bạn truy xuất qua mảng kết quả hoặc qua $1, $2 trong replace.

Bắt giữ và đổi vị trí ngày tháng
const ngay = "25/06/2026";
const re = /(\d{2})\/(\d{2})\/(\d{4})/;
const m = ngay.match(re);
console.log(m[1], m[2], m[3]); // "25" "06" "2026"

// Dùng $1 $2 $3 để đảo sang định dạng ISO
console.log(ngay.replace(re, "$3-$2-$1")); // "2026-06-25"

Non-capturing & Alternation

Khi chỉ cần gom nhóm để áp lượng từ mà không muốn lưu, dùng (?:...) để tiết kiệm bộ nhớ và giữ số thứ tự nhóm gọn gàng. Dấu alternation | nghĩa là "hoặc", thường kết hợp với nhóm.

Non-capturing và alternation
// (?:...) gom nhóm nhưng KHÔNG bắt giữ
const re = /(?:https?|ftp):\/\/(\w+)/;
console.log("https://jstools".match(re)[1]); // "jstools" — nhóm 1 vẫn là tên miền

// | nghĩa là "hoặc"
console.log("mèo chó cá".match(/mèo|chó/g)); // ["mèo", "chó"]

Named groups & Backreferences

Named group (?<name>...) giúp đặt tên cho nhóm để truy xuất qua match.groups.name — dễ đọc hơn nhiều so với đếm số. Backreference \1 (hoặc \k<name>) cho phép khớp lại đúng nội dung mà một nhóm đã bắt giữ trước đó — rất hữu ích để tìm từ lặp hoặc thẻ trùng khớp.

Named groups và backreference
// Named group: truy xuất bằng tên thay vì số
const m = "2026-06-25".match(/(?<nam>\d{4})-(?<thang>\d{2})-(?<ngay>\d{2})/);
console.log(m.groups.nam, m.groups.ngay); // "2026" "25"

// Backreference \1: tìm từ bị lặp lại liên tiếp
console.log("the the cat cat".match(/\b(\w+)\s+\1\b/g)); // ["the the","cat cat"]

5. Lookahead & Lookbehind

Lookaround (nhìn quanh) là các "khẳng định độ dài 0": chúng kiểm tra xem phía trước hoặc phía sau vị trí hiện tại có/không có một mẫu, nhưng không tiêu thụ ký tự nào vào kết quả. Có bốn loại:

  • (?=...)positive lookahead: phía sau PHẢI có mẫu này.
  • (?!...)negative lookahead: phía sau KHÔNG được có mẫu này.
  • (?<=...)positive lookbehind: phía trước PHẢI có mẫu này.
  • (?<!...)negative lookbehind: phía trước KHÔNG được có mẫu này.

Ứng dụng kinh điển nhất là kiểm tra mật khẩu mạnh: gộp nhiều lookahead ở đầu chuỗi để yêu cầu đồng thời nhiều điều kiện (phải có chữ hoa, có chữ số, có ký tự đặc biệt) mà không cần quan tâm thứ tự xuất hiện của chúng.

Kiểm tra mật khẩu mạnh bằng lookahead
// Yêu cầu: tối thiểu 8 ký tự, có chữ thường, chữ hoa, chữ số và ký tự đặc biệt
const matKhau = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%]).{8,}$/;

console.log(matKhau.test("abc123"));        // false — thiếu hoa và đặc biệt
console.log(matKhau.test("Abcd1234!"));     // true
Lookbehind cho tiền tệ và lookahead phủ định
// Lookbehind: lấy số tiền đứng sau dấu $ (không lấy luôn dấu $)
console.log("Giá $250 và €99".match(/(?<=\$)\d+/)); // ["250"]

// Negative lookahead: từ "foo" nhưng KHÔNG theo sau bởi "bar"
console.log("foobar foobaz".match(/foo(?!bar)/g)); // ["foo"] (chỉ trong foobaz)

6. Các phương thức String với Regex

Regex chỉ thực sự hữu dụng khi kết hợp với các phương thức của StringRegExp. Dưới đây là bộ công cụ bạn sẽ dùng hằng ngày.

  • str.match(re) — không có cờ g trả về thông tin chi tiết (kèm nhóm); có cờ g trả về mảng tất cả chuỗi khớp.
  • str.matchAll(re) — trả về iterator chứa mọi kết quả khớp kèm nhóm và index (bắt buộc cờ g).
  • str.replace(re, x) / str.replaceAll(re, x) — thay thế bằng chuỗi (dùng $1, $&) hoặc bằng hàm callback linh hoạt.
  • str.split(re) — tách chuỗi theo một mẫu phân tách.
  • re.test(str) — trả về true/false, nhanh nhất để kiểm tra hợp lệ.
  • re.exec(str) — gọi lặp với cờ g sẽ tự dịch chuyển lastIndex qua từng kết quả.
matchAll với index và nhóm
const text = "SP-12 và SP-45 còn hàng";
for (const m of text.matchAll(/SP-(\d+)/g)) {
  console.log(`Mã ${m[1]} tại vị trí ${m.index}`);
}
// Mã 12 tại vị trí 0
// Mã 45 tại vị trí 9
replace với hàm callback
// Callback nhận từng kết quả khớp và trả về chuỗi thay thế động
const out = "giá 1000 và 2500 đồng".replace(/\d+/g, (so) => {
  return Number(so).toLocaleString("vi-VN");
});
console.log(out); // "giá 1.000 và 2.500 đồng"
exec với lastIndex
const re = /\d+/g;     // BẮT BUỘC có cờ g để lastIndex tiến lên
const str = "a1b22c333";
let m;
while ((m = re.exec(str)) !== null) {
  console.log(`Khớp "${m[0]}", lastIndex giờ là ${re.lastIndex}`);
}
// Khớp "1", lastIndex giờ là 2
// Khớp "22", lastIndex giờ là 5
// Khớp "333", lastIndex giờ là 9

Hãy tự tay thử nghiệm replace với callback và named group trong sân chơi dưới đây:

▶ Thử chạy: replace với callback & named group

7. Ứng dụng thực tế

Regex tỏa sáng trong các tác vụ kiểm tra hợp lệ (validation), trích xuất (extraction)tô sáng (highlight). Dưới đây là vài mẫu thực dụng cho email, số điện thoại Việt Nam và URL.

Validate email, phone, URL
const reEmail = /^[\w.+-]+@[\w-]+\.[\w.-]+$/;
const rePhoneVN = /^(0|\+84)(3|5|7|8|9)\d{8}$/;
const reURL = /^https?:\/\/[\w.-]+(\/\S*)?$/;

console.log(reEmail.test("[email protected]")); // true
console.log(rePhoneVN.test("0901234567"));     // true
console.log(reURL.test("https://js-tools.org/blog")); // true
Trích xuất hashtag và tô sáng từ khóa
const post = "Học #JavaScript và #Regex thật vui #coding";
console.log(post.match(/#\w+/g)); // ["#JavaScript","#Regex","#coding"]

// Bọc từ khóa trong thẻ <mark> để tô sáng kết quả tìm kiếm
function highlight(text, tu) {
  const re = new RegExp(`(${tu})`, "gi");
  return text.replace(re, "<mark>$1</mark>");
}
console.log(highlight("Tôi yêu regex", "regex"));
// "Tôi yêu <mark>regex</mark>"
[!CAUTION] Đừng dùng Regex để phân tích HTML/XML! HTML là ngôn ngữ lồng nhau, đệ quy và đầy ngoại lệ — vượt khỏi khả năng diễn đạt của biểu thức chính quy thông thường. Một mẫu "có vẻ chạy" sẽ vỡ ngay khi gặp thẻ lồng, comment, hoặc thuộc tính có dấu ngoặc. Hãy dùng DOMParser hoặc document.querySelector cho HTML. Tương tự, để xác thực email tuyệt đối chính xác theo chuẩn RFC, hãy dùng thư viện chuyên dụng — Regex chỉ nên dùng để kiểm tra sơ bộ.

Thử kiểm tra hợp lệ với .test() ngay trong sân chơi dưới đây:

▶ Thử chạy: kiểm tra email & số điện thoại

8. Câu hỏi trắc nghiệm ôn tập

Câu 1

Cờ nào khiến Regex tìm tất cả kết quả khớp thay vì chỉ dừng ở kết quả đầu tiên?

Câu 2

Trong chuỗi <b>hi</b>, mẫu /<.+?>/ (lazy) sẽ khớp được gì?

Câu 3

Cú pháp nào là positive lookahead — khẳng định "phía sau phải có mẫu này"?

Tải file code thực hành minh họa bài học

Chúng tôi đã viết sẵn một file script hoàn chỉnh tổng hợp mọi kỹ thuật trong bài: character classes, quantifiers greedy/lazy, groups & backreferences, lookahead/lookbehind, các phương thức String với Regex và bộ mẫu validate thực tế:

Tải về js_regex.js

Related Articles

Bài viết liên quan trong series

Lesson 8: Asynchronous JS: Iterators, Generators & Event Loop Bài 8: Iterators, Generators & Bất Đồng Bộ — Promises & Async/Await Lesson 6: Error Handling & Debugging in JavaScript Bài 6: Xử Lý Lỗi & Gỡ Rối trong JavaScript Back to JavaScript Series Overview Quay lại Lộ trình JavaScript Series