“String”对象
本文介绍了“String”对象。
解释内容涵盖了从基础到高级技术,包括与Unicode和正则表达式相关的陷阱,循序渐进、易于理解。
YouTube Video
“String”对象
在日常开发中,JavaScript中的字符串是最常用的数据类型之一。
原始字符串和String对象之间的区别
原始字符串(如 '"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的字符串处理有一些易被忽略的规范和行为。为避免意外的bug,请牢记以下几点。
- 使用“new String()”可能导致类型检查或比较结果不正确。大多数情况下,原始字符串类型已经足够。
- 在Unicode中,单个可见字符可能由多个码元组成。因此,“length”返回值可能与实际显示字符数不符。
- 将用户输入用于正则表达式前,一定要先进行转义。
- “String.prototype.replace()”默认只替换第一个匹配项。如果你想替换所有出现的内容,请在正则表达式中使用
/g标志。 - 字符串是不可变的,因此相关操作总是返回一个新字符串。务必注意分配返回的新值。
总结
尽管JavaScript字符串看似简单,但了解其Unicode和不可变特性至关重要。熟练掌握基础知识能大幅提升字符串处理的可靠性和可读性。
您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。