`Đối tượng` String ``
Bài viết này giải thích về đối tượng String.
Bài giải thích này bao gồm mọi thứ từ cơ bản đến kỹ thuật nâng cao, bao gồm cả những cạm bẫy liên quan đến Unicode và biểu thức chính quy, từng bước một và dễ hiểu.
YouTube Video
Đối tượng String ``
Chuỗi trong JavaScript là một trong những kiểu dữ liệu được sử dụng thường xuyên nhất trong phát triển hàng ngày.
Sự khác biệt giữa chuỗi nguyên thủy và đối tượng String
Chuỗi nguyên thủy (ví dụ như "hello") hành xử khác với đối tượng bao như new String("hello"). Thông thường, bạn nên sử dụng kiểu nguyên thủy, và hiếm khi cần dùng dạng đối tượng.
1// Primitive string
2const a = "hello";
3
4// String wrapper object
5const b = new String("hello");
6
7console.log(typeof a); // "string"
8console.log(typeof b); // "object"
9console.log(a === b); // false — wrapper objects are not strictly equal
- Đoạn mã này cho thấy sự khác biệt về kiểu giữa chuỗi nguyên thủy và bao, cũng như cách chúng hoạt động với so sánh nghiêm ngặt. Trong hầu hết các trường hợp, tránh dùng
new String()và chỉ sử dụng kiểu nguyên thủy.
Cách tạo chuỗi (chuỗi ký tự và chuỗi mẫu)
Chuỗi mẫu (template literals) hữu ích để nhúng biến và viết chuỗi nhiều dòng. Bạn có thể chèn biến và đánh giá biểu thức một cách trực quan.
1const name = "Alice";
2const age = 30;
3
4// Template literal
5const greeting = `Name: ${name}, Age: ${age + 1}`;
6
7console.log(greeting); // "Name: Alice, Age: 31"
- Chuỗi mẫu có tính dễ đọc cao và lý tưởng để xây dựng các chuỗi phức tạp, kể cả chuỗi nhiều dòng.
Các phương thức thông dụng (Tìm kiếm và Trích xuất chuỗi con)
Đối tượng String có rất nhiều phương thức cơ bản.
1const text = "Hello, world! Hello again.";
2
3// search
4console.log(text.indexOf("Hello")); // 0
5console.log(text.indexOf("Hello", 1)); // 13
6console.log(text.includes("world")); // true
7console.log(text.startsWith("Hello")); // true
8console.log(text.endsWith("again.")); // true
9
10// slice / substring
11console.log(text.slice(7, 12)); // "world"
12console.log(text.substring(7, 12)); // "world"
slicevàsubstringgiống nhau, nhưng chúng xử lý chỉ số âm khác nhau.slicehiểu các giá trị âm là vị trí tính từ cuối chuỗi. Hãy rõ ràng về việc sẽ sử dụng cái nào.
Tách và Nối chuỗi (split / join)
Việc tách một chuỗi thành mảng để xử lý rồi nối lại là rất phổ biến.
1const csv = "red,green,blue";
2const arr = csv.split(","); // ["red","green","blue"]
3
4console.log(arr);
5console.log(arr.join(" | ")); // "red | green | blue"
- Một mẫu thường gặp là dùng
splitđể tách chuỗi, xử lý mảng kết quả bằngmaphoặcfilter, sau đó dùngjoinđể nối lại.
Thay thế và Biểu thức chính quy
replace chỉ thay thế kết quả khớp đầu tiên. Nếu muốn thay thế tất cả kết quả khớp, hãy dùng cờ g với biểu thức chính quy. Bằng cách truyền một hàm làm giá trị thay thế, bạn cũng có thể thực hiện thay thế động.
1const s = "foo 1 foo 2";
2
3// replace first only
4console.log(s.replace("foo", "bar")); // "bar 1 foo 2"
5
6// replace all using regex
7console.log(s.replace(/foo/g, "bar")); // "bar 1 bar 2"
8
9// replace with function
10const r = s.replace(/\d+/g, (match) => String(Number(match) * 10));
11console.log(r); // "foo 10 foo 20"
- Với việc thay thế động bằng hàm, bạn có thể viết mã phân tích và biến đổi các kết quả khớp một cách ngắn gọn.
Chuyển đổi và Chuẩn hóa chữ hoa/thường
Để hỗ trợ và so sánh đa ngôn ngữ, ngoài toLowerCase và toUpperCase, việc chuẩn hóa Unicode (normalize) cũng rất quan trọng. Điều này đặc biệt cần thiết khi so sánh các ký tự có dấu.
1// Case conversion example:
2// "\u00DF" represents the German letter "ß".
3// In some locales, converting "ß" to uppercase becomes "SS".
4// JavaScript follows this behavior.
5console.log("\u00DF");
6console.log("\u00DF".toUpperCase()); // "SS"
7
8// Unicode normalization example:
9// "e\u0301" is "e" + a combining acute accent.
10// "\u00e9" is the precomposed character "é".
11// These two look the same but are different code point sequences.
12const a = "e\u0301";
13const b = "\u00e9";
14
15console.log(a === b); // false: different underlying code points
16console.log(a.normalize() === b.normalize()); // true: normalized to the same form
- Các biểu diễn Unicode khác nhau như chữ ghép hay ký tự tổ hợp sẽ không bằng nhau nếu giữ nguyên, nên hãy dùng
normalize()trước khi so sánh.
Unicode và điểm mã (Xử lý cặp đại diện)
Chuỗi trong JavaScript là tập hợp các đơn vị mã UTF-16, nên một số ký tự như emoji có thể chiếm hai đơn vị mã cho một ký tự. Để xử lý đơn vị ký tự thực, hãy dùng Array.from, toán tử spread hoặc for...of.
1// Emoji composed with multiple code points:
2// "\u{1F469}" = woman, "\u{200D}" = Zero Width Joiner (ZWJ),
3// "\u{1F4BB}" = laptop. Combined, they form a single emoji: 👩💻
4const s = "\u{1F469}\u{200D}\u{1F4BB}";
5console.log(s);
6
7// Length in UTF-16 code units (not actual Unicode characters):
8// Because this emoji uses surrogate pairs + ZWJ, the length may be > 1.
9console.log("Length:", s.length);
10
11// Iterate by Unicode code points (ES6 for...of iterates code points):
12// Each iteration gives a full Unicode character, not UTF-16 units.
13for (const ch of s) {
14 console.log(ch);
15}
16
17// Convert to an array of Unicode characters:
18console.log(Array.from(s));lengthtrả về số đơn vị mã, vì vậy bạn có thể không có số lượng ký tự như mong muốn với emoji hoặc chữ ghép.for...ofvàArray.fromxử lý gần giống như các ký tự hiển thị (cụm đồ thị), nhưng nếu bạn cần hỗ trợ đồ thị đầy đủ, hãy cân nhắc sử dụng thư viện chuyên dụng.
Thay thế biểu thức chính quy an toàn (Khi xử lý đầu vào của người dùng)
Nếu bạn quên thoát dữ liệu do người dùng nhập khi nhúng vào biểu thức chính quy, điều này có thể dẫn đến hành vi không mong muốn và các lỗ hổng bảo mật. Luôn escape dữ liệu người dùng trước khi sử dụng trong biểu thức.
1function escapeRegex(s) {
2 return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3}
4
5const userInput = "a+b";
6const pattern = new RegExp(escapeRegex(userInput), "g");
7console.log("a+b a+b".replace(pattern, "X")); // "X X"
- Đừng dùng chuỗi của người dùng trực tiếp trong biểu thức chính quy; hãy luôn escape chúng trước khi tạo regex.
Kinh nghiệm hiệu năng: Nối chuỗi và chuỗi mẫu
Khi nối nhiều chuỗi nhỏ liên tiếp, cho chúng vào một mảng và dùng join có thể hiệu quả hơn. Ngược lại, chuỗi mẫu rất dễ đọc và đủ nhanh trong hầu hết các trường hợp.
1// concatenation in loop (less ideal)
2let s = "";
3for (let i = 0; i < 1000; i++) {
4 s += i + ",";
5}
6
7// using array + join (often faster for many pieces)
8const parts = [];
9for (let i = 0; i < 1000; i++) {
10 parts.push(i + ",");
11}
12const s2 = parts.join("");- Các máy JavaScript hiện đại đã tối ưu rất tốt, nên bạn không cần lo lắng về hiệu năng với số lần nối chuỗi nhỏ. Tuy nhiên, nếu bạn cần nối hàng chục nghìn lần, sử dụng
joinsẽ hiệu quả hơn.
Các kỹ thuật thực tế hữu ích: Thêm ký tự, Loại bỏ khoảng trắng và Lặp lại
trim, padStart, padEnd, và repeat là những phương thức tiện lợi, đặc biệt hữu ích trong xử lý chuỗi hàng ngày. Chúng thường được sử dụng trong các tình huống thực tế như định dạng giá trị đầu vào hoặc chuẩn hóa định dạng đầu ra.
1console.log(" hello ".trim()); // "hello"
2console.log("5".padStart(3, "0")); // "005"
3console.log("x".repeat(5)); // "xxxxx"
- Những phương thức này có thể dùng để chuẩn hóa đầu vào biểu mẫu hoặc tạo đầu ra với chiều rộng cố định.
So sánh chuỗi (So sánh theo ngôn ngữ)
localeCompare rất hiệu quả khi so sánh chuỗi dựa theo thứ tự từ điển của các ngôn ngữ khác nhau. Bạn có thể chỉ định ngôn ngữ và các tùy chọn về độ nhạy (như phân biệt chữ hoa/thường).
1console.log(
2 "\u00E4".localeCompare("z", "de")
3); // may be -1 or other depending on locale
4
5console.log(
6 "a".localeCompare("A", undefined, { sensitivity: "base" })
7); // 0
- Để so sánh quốc tế hóa, hãy dùng
localeComparevà chỉ định địa phương và tùy chọn phù hợp.
Ví dụ thực tế: Chuyển một dòng CSV thành đối tượng (Quy trình thực tế)
Một trường hợp sử dụng phổ biến là phân tích một dòng CSV thành đối tượng bằng cách kết hợp split, trim và map. Với các trường có dấu ngoặc kép hoặc tệp CSV phức tạp, hãy sử dụng thư viện phân tích CSV chuyên dụng.
1// simple CSV parse (no quotes handling)
2function parseCsvLine(line, headers) {
3 const values = line.split(",").map(v => v.trim());
4 const obj = {};
5 headers.forEach((h, i) => obj[h] = values[i] ?? null);
6 return obj;
7}
8
9const headers = ["name", "age", "city"];
10const line = " Alice , 30 , New York ";
11console.log(parseCsvLine(line, headers));
12// { name: "Alice", age: "30", city: "New York" }
- Phương pháp này áp dụng cho CSV đơn giản, nhưng lưu ý rằng nó không xử lý được trường hợp có dấu phẩy bên trong trường được đặt trong ngoặc kép.
Các lỗi thường gặp
Có một số đặc tả và hành vi dễ bị bỏ qua trong xử lý chuỗi JavaScript. Để tránh lỗi không mong muốn, bạn nên lưu ý những điểm sau.
- Việc sử dụng
new String()có thể dẫn đến kết quả sai khi kiểm tra kiểu hoặc so sánh. Trong hầu hết trường hợp, kiểu chuỗi nguyên thủy là đủ. - Trong Unicode, một ký tự hiển thị có thể gồm nhiều đơn vị mã. Do đó, giá trị mà
lengthtrả về có thể không đúng với số ký tự hiển thị thực tế. - Khi đưa dữ liệu người dùng vào biểu thức chính quy, hãy luôn escape trước.
String.prototype.replace()mặc định chỉ thay thế kết quả khớp đầu tiên. Nếu bạn muốn thay thế tất cả các lần xuất hiện, hãy sử dụng cờ/gtrong biểu thức chính quy của bạn.- Chuỗi là bất biến, nên mọi thao tác luôn trả về một chuỗi mới. Rất quan trọng là luôn gán giá trị trả về.
Tóm tắt
Dù chuỗi trong JavaScript có vẻ đơn giản, bạn cần hiểu rõ các đặc điểm về Unicode và tính bất biến của chúng. Bằng cách nắm vững các kiến thức cơ bản, bạn có thể nâng cao độ tin cậy và khả năng đọc hiểu trong xử lý chuỗi của mình.
Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.