`String` 物件
本文將說明 String 物件。
說明從基礎到進階技術都包含,內容包含與 Unicode 和正規表達式相關的陷阱,並以易於理解的方式逐步解說。
YouTube Video
String 物件
在 JavaScript 開發中,字串是日常開發中最常用的型別之一。
基本型字串與字串物件的差異
基本型字串(如 "hello")與包裝物件(如 new String("hello"))的行為不同。通常應該使用基本型,幾乎沒有需要用物件型的情況。
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
- 這段程式碼顯示了基本型與包裝型在型別上的差異,以及它們在嚴格比較時的行為。大多數情況下,請避免使用
new String(),僅用基本型即可。
建立字串的方法(字面值與樣板字串)
樣板字串便於嵌入變數並撰寫多行字串。可以直覺地插入變數並計算表達式。
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"
- 樣板字串可讀性高,非常適合建立複雜或多行字串。
常用方法(搜尋與擷取子字串)
String 物件具有許多基本方法。
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"
slice和substring類似,但對負索引的處理方式不同。slice會將負值解讀為從字串尾端起算的位置。請明確選擇要使用哪一個方法。
分割與合併(split / join)
將字串拆分為陣列進行處理,之後再合併回來是很常見的做法。
1const csv = "red,green,blue";
2const arr = csv.split(","); // ["red","green","blue"]
3
4console.log(arr);
5console.log(arr.join(" | ")); // "red | green | blue"
- 常見的流程是先用
split拆分字串,再用map或filter處理陣列,最後用join合併回字串。
取代與正則表達式
replace 預設只會取代第一個匹配項目。如果要取代所有匹配,需配合正規表達式 g 標誌。若將函數作為替換項目傳入,亦可進行動態取代。
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"
- 利用函數進行動態替換時,可以簡潔地寫出解析與轉換匹配內容的程式碼。
大小寫轉換與標準化
為了支援多語言與比較,除了 toLowerCase 和 toUpperCase 之外,Unicode 正規化(normalize)也很重要。特別是在比較帶有重音的字元時尤其需要。
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
- 不同的 Unicode 表示方式(如連字、組合字元),比對時可能不相等,因此比較前應先用
normalize()標準化。
Unicode 與碼點(代理對的處理)
JavaScript 的字串是由 UTF-16 編碼單元組成,因此有些字元(如表情符號)一個字會佔用兩個編碼單元。若要以實際字元單位處理,請使用 Array.from、展開運算符或 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));length是回傳編碼單元數量,對於表情符號或連字,結果可能與預期數量不同。for...of與Array.from處理的單位接近顯示字元(字位群),若需要完整支援請考慮使用專門的函式庫。
正規表達式下的安全替換(處理用戶輸入時)
如果您在將使用者輸入嵌入正規表達式時忘記跳脫,可能會導致不可預期的行為和漏洞。在輸入內容用於模式之前,一定要先進行跳脫處理。
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"
- 切勿將用戶字串直接用於正規表達式;在產生正規表達式前務必先跳脫。
效能建議:串接與樣板字串
大量串接字串時,先收集到陣列再用 join 合併,效能更佳。另一方面,樣板字串可讀性高並且在大多數情況下速度也很快。
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("");- 現代 JavaScript 引擎優化良好,少量串接時通常不需擔心效能。但如果需要串接數萬次以上,使用
join會更有效率。
實用技巧:補齊、去頭尾空白與重複
trim、padStart、padEnd 和 repeat 是非常方便的方法,在日常的字串處理中特別有用。它們經常用於實際情境,例如格式化輸入值或標準化輸出格式。
1console.log(" hello ".trim()); // "hello"
2console.log("5".padStart(3, "0")); // "005"
3console.log("x".repeat(5)); // "xxxxx"
- 這些方法可用於表單輸入的規範化或生成固定寬度的輸出。
字串比對(區域性比對)
localeCompare 非常適合以字典順序針對不同語言進行字串比對。你可以指定語言和敏感度選項(如區分大小寫等)。
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
- 如需國際化比對,請使用
localeCompare並指定正確的地區與選項。
實用範例:將一行 CSV 轉換為物件(實務流程)
常見作法是以 split、trim、map 等組合將一行 CSV 解析成物件。若需要處理帶引號欄位或複雜 CSV,請使用專門的 CSV 解析器。
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" }
- 這個方法適用於簡單的 CSV,但無法處理引號內有逗號的狀況。
常見陷阱
JavaScript 字串處理中有一些容易忽略的規範與行為。為避免意外錯誤,請特別注意以下幾點。
- 使用
new String()可能導致型別判斷或比對結果錯誤。絕大多數情況下,基本型字串已經足夠。 - 在 Unicode 中,一個可見字元可能包含多個編碼單元。因此
length回傳值不一定等於實際顯示的字元數量。 - 將用戶輸入用於正規表達式時,請務必先跳脫。
String.prototype.replace()預設只取代第一個符合的字串。如果您想要替換所有出現的項目,請在您的正規表達式中使用/g標誌。- 字串是不可變的,所以每次操作會回傳新字串。務必記得將回傳結果接收下來。
總結
雖然 JavaScript 字串看似簡單,但 Unicode 與不可變等特性務必理解清楚。只要掌握基礎,就能大幅提升字串處理的可靠性與可讀性。
您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。