B 币安 · 全球最大的加密货币交易所币安 注册 → AD OKX OKX · 全球领先的加密货币交易所OKX 注册 → AD
💻 编程基础 · 第7讲 / 10

字符串与文本处理——截取、查找、替换文字

字符串是按顺序排列的一串字符,JavaScript 自带截取、查找、拆分、替换等方法。要点有两个:方法不会修改原来的字符串,而是返回新字符串;`length` 并不总等于“眼睛看到的字数”。

⏱ 约20分钟 ✍️ 练习题 5 道 🧪 代码练习 4 题 更新 2026-10-09
🎯 本讲学习目标
  • 能用 length、索引和 slice 取出字符串的一部分
  • 能用 indexOf、includes、startsWith 判断某段文字是否存在、在什么位置
  • 能用 split、join、trim、replaceAll 拆分、整理和替换文本
  • 能说明汉字和表情符号的 length 为什么可能与看到的字数不同,并能读懂简单的正则表达式

1.字符串是一串字符

第2讲说过,字符串(string)就是用引号括起来的值。双引号 "苹果" 和单引号 '苹果' 是同一个字符串;用反引号括起来的模板字符串(模板字面量)则用来把变量的值嵌进字符串里。模板字符串长什么样,请直接看下面的代码。不管用哪种引号,在同一个程序里统一成一种会更好读。

字符串是按顺序排列的一串字符,所以可以像第6讲的数组一样,用编号(索引)逐个取出字符。第一个字符是0号,s.length 是以字符为单位的长度。因此最后一个字符是 s[s.length - 1],现在也常用 s.at(-1) 这种从后往前数的写法。取一个不存在的编号时不会报错,而是得到 undefined,这一点也和数组一样。

字符串和数组有一个很大的不同:字符串一旦创建,里面的字符就不能修改。如果像 s[0] = "z" 这样想改其中一个字,在严格模式下运行的代码(比如这个练习场)里会出现 TypeError。所以本讲的所有方法都不会改动原字符串,而是返回一个“新字符串”。要使用结果,就必须用变量接住它。

索引从0开始,不存在的编号得到 undefined
const name = "小明";
const fruit = '苹果';
const msg = `${name}买了${fruit}3个`;
console.log(msg);
console.log(msg.length);
console.log(msg[0], msg.at(-1), msg[99]);
输出
小明买了苹果3个
8
小 个 undefined
方法返回新字符串——不接住就还是原样
let s = "  hello  ";
s.trim();
console.log("[" + s + "]");
s = s.trim();
console.log("[" + s + "]");
输出
[  hello  ]
[hello]
调用了方法却发现结果没变时,先检查有没有把返回值存进变量。这是初学者最常犯的错误。

2.字数的陷阱——汉字与表情符号

length 数的不是“看得见的字数”,而是 JavaScript 存储字符串的单位(UTF-16 编码单元)的个数。好在日常使用的常用汉字一个字就是一个单元,所以 "汉字".length 是2,"编程很有趣".length 是5,和看到的一样。英文字母、数字、空格和标点也是一个字一个单元。

表情符号就不同了。很多表情符号占两个单元,"😀".length 是2。像带肤色的手势这种由几块拼成、看起来是一个图形的表情符号,长度可能是4甚至更多。把字符串像 [...s] 这样展开,会按码点拆分,一个 😀 就算1;但拼合而成的表情符号展开后仍然按块数计算。要按屏幕上看到的图形精确计数,需要 Intl.Segmenter 这类专门的工具。

汉字也不是永远可以放心。收录在扩展区的生僻字(例如有些人名里用到的“𠮷”)不在常用范围内,一个字要占两个单元,length 是2。另外,同一个带音调符号的字母,比如 é,既可以存成一个字符,也可以存成“e + 音调符号”两个部分(分解形式,NFD),后者的长度是2。从某些程序里复制过来的文件名有时就是这种形式,所以会出现外表相同、=== 比较却是 false 的情况;韩文等文字也有同样的问题。这时先用 s.normalize("NFC") 统一成一种形式再比较。

还有一点:字数和字节数是两回事。在文件和网络中常用的 UTF-8 编码下,一个常用汉字通常占3个字节。如果输入框限制的是“最多90字节”这样的字节数,就不能用 length 来数。总结一下:以常用汉字为主的普通句子用 length 计数没问题,但一旦混入表情符号、生僻字、分解形式的字符或字节限制,就要先想清楚自己到底在数什么。

在这个练习场里实际运行的结果
console.log("汉字".length, "编程很有趣".length);
console.log("😀".length, [..."😀"].length);
console.log("👍🏽".length, [..."👍🏽"].length);
输出
2 5
2 1
4 2
分解形式的字符与 UTF-8 字节数
const a = "é";
const b = a.normalize("NFD");
console.log(a.length, b.length);
console.log(a === b);
console.log(a === b.normalize("NFC"));
const bytes = new TextEncoder().encode("汉字");
console.log(bytes.length);
输出
1 2
false
true
6
数法不同,“字数”就不同(在这个练习场里确认过的值)
字符串`length``[...s].length`UTF-8 字节
"汉字"226
"abc"333
"😀"214
"👍🏽"428

3.截取与查找——`slice`、`indexOf`、`includes`

s.slice(开始, 结束) 从开始编号截到结束编号的“前一个”,作为新字符串返回。省略结束就截到最后;给负数则从后往前数。所以 s.slice(0, 3) 是前三个字,s.slice(-4) 是后四个字。不包含结束编号这条规则,和第4讲循环里写 i < n 是同一种感觉,长度可以直接用“结束 − 开始”算出来。

查找主要用三种方法。s.indexOf("要找的词") 返回第一次出现的位置编号,找不到就返回 -1。如果不需要位置、只想知道有没有,s.includes("要找的词") 会返回 true 或 false,很适合直接放进第3讲的条件语句。startsWith 和 endsWith 检查开头或结尾是不是那个词,比如确认文件名是否以 ".csv" 结尾。

把 indexOf 的结果直接当条件用会掉进陷阱。要找的词在最前面时结果是 0,而第3讲说过 0 是假值,所以 if (s.indexOf("x")) 恰恰在“出现在最前面”时为假。只想判断有没有时用 includes;如果用 indexOf,就和 !== -1 比较。另外,这些方法区分大小写,"Apple".includes("apple") 是 false。要忽略大小写,先用 toLowerCase() 把两边统一,再查找。

不包含结束编号
const phone = "138-1234-5678";
console.log(phone.slice(0, 3));
console.log(phone.slice(-4));
console.log(phone.slice(4, 8));
输出
138
5678
1234
const file = "2026_销售记录.csv";
console.log(file.indexOf("_"));
console.log(file.indexOf("x"));
console.log(file.includes("销售"));
console.log(file.endsWith(".csv"));
console.log("Apple".includes("apple"));
const t = "Apple".toLowerCase();
console.log(t.includes("apple"));
输出
4
-1
true
true
false
true
例题要从示例邮箱地址 "[email protected]" 中只取出 @ 前面的用户名,该怎么做?
const email = "[email protected]";
const at = email.indexOf("@");
if (at === -1) {
  console.log("不是邮箱格式");
} else {
  console.log(email.slice(0, at));
}
输出
minsu
  1. 第1步 找到 @ 的位置。indexOf("@") 是5(m-i-n-s-u 是0到4号)。
  2. 第2步 从0号截到5号的“前一个”即可。也就是 slice(0, 5),长度是 5 − 0 = 5个字。
  3. 第3步 如果传进来的地址没有 @,indexOf 会得到 -1,slice(0, -1) 就成了去掉最后一个字的错误结果。所以要先加一个判断是否为 -1 的条件。
答案email.slice(0, email.indexOf("@"))——不过要先排除没有 @ 的情况。

4.拆分、拼接、修整——`split`、`join`、`trim`

文本处理有一半靠 split 和 join。s.split(",") 以逗号为界把字符串切开,返回一个数组;arr.join("-") 反过来把数组连成一个字符串。字符串变成数组后,第6讲学过的 length、map、filter 都能用上,所以“拆分 → 当数组处理 → 再拼回去”就成了基本流程。第9讲的自动化例子也建立在这个流程上。

split 有几个需要知道的边角情况。拆分空字符串 "" 得到的不是空数组,而是装着一个空字符串的数组 [ '' ]。两个逗号连在一起时,中间会多出一个空字符串。所以如果用 split(" ").length 数“有几个词”,空输入会算成1个,空格重叠的地方数量也会变多。这就是一定要同时测试边界值的原因。

trim() 去掉首尾的空格、制表符和换行(中间的空格不动)。人输入的值前后经常带着空格,比较或保存前先过一遍 trim() 是好习惯。toUpperCase() 和 toLowerCase() 转换英文字母的大小写,汉字这类没有大小写的字符保持不变。padStart(3, "0") 在前面补 "0" 直到长度为3,把 "7" 变成 "007",在按位数对齐显示编号或时间时很常用。

拆开是数组,拼起来是字符串
const line = "小明,苹果,3";
const cols = line.split(",");
console.log(cols);
console.log(cols.length, cols[1]);
console.log(cols.join(" / "));
输出
[ '小明', '苹果', '3' ]
3 苹果
小明 / 苹果 / 3
拆分空字符串得到 [ '' ]
console.log("".split(","));
console.log("a,,b".split(","));
console.log("  Hi  ".trim().toUpperCase());
console.log("7".padStart(3, "0"));
console.log("甲乙丙".toUpperCase());
输出
[ '' ]
[ 'a', '', 'b' ]
HI
007
甲乙丙
  • split("")(以空字符串为界)按单元逐个切开。混有表情符号时可能把它切坏,所以按字切分时用 [...s] 更安全。
  • split、trim、toUpperCase 都返回新值,原字符串保持不变。
  • 按行拆分用 split("\n")。在 Windows 上生成的文件,行尾可能多一个看不见的字符,所以拆分后对每行再 trim() 一下更稳妥。

5.替换——`replace` 与 `replaceAll`

s.replace("要找的词", "替换成的词") 只替换第一次出现的那一处。要全部替换就用 replaceAll。正因为这个区别,“怎么只换了第一个?”的情况经常出现,所以写好替换代码后,一定要用同一个词出现两次以上的输入试一试。两个方法都不改原字符串,而是返回替换后的新字符串。

替换也可以用来“删除”。把替换成的词设为空字符串 "",找到的部分就会消失。例如 "138-1234-5678".replaceAll("-", "") 的结果是 "13812345678"。反过来,想给只有数字的电话号码加上连字符,用 slice 切开再用 join 拼起来更干净。不管做什么处理,原文都保持不变、另外生成新值,所以很容易把处理前后并排对比。

replace 只换第一个
const s = "苹果-梨-苹果";
console.log(s.replace("苹果", "橘子"));
console.log(s.replaceAll("苹果", "橘子"));
console.log(s);
输出
橘子-梨-苹果
橘子-梨-橘子
苹果-梨-苹果
删掉再拼回去
const raw = "138-1234-5678";
const digits = raw.replaceAll("-", "");
console.log(digits);
const parts = [
  digits.slice(0, 3),
  digits.slice(3, 7),
  digits.slice(7),
];
console.log(parts.join("-"));
输出
13812345678
138-1234-5678
替换前后的文本很长时,用眼睛很难对比。把两份结果贴进文本对比工具,只有改动的地方会被标出来,很快就能确认有没有改到不该改的地方。

6.初尝正则表达式——按形状查找

到目前为止我们找的都是“正好是这个词”。想按形状查找,比如“连在一起的数字”“连续好几个空格的地方”,就要用正则表达式。在 JavaScript 里,模式写在两个 / 之间,后面跟的 g 表示“不是第一个,而是全部”。正则表达式本身就是一门小语言,这里只看几个常用写法,达到能读懂的程度。

\d 表示一个数字,\s 表示一个空白字符(空格、制表符、换行),后面跟的 + 表示“一个或多个连在一起”。所以 /\d+/g 是“所有连在一起的数字块”,/\s+/g 是“所有连续的空白”。match 把找到的块作为数组返回;注意一个都没找到时返回的不是空数组,而是 null。test 只用 true 或 false 告诉你找没找到。

正则表达式很强大,但难读,稍有差错就会匹配到不该匹配的地方,或者漏掉该匹配的。原则有两条。第一,能用 includes、split、replaceAll 完成的事就用它们,只在确实需要时才用正则。第二,不管是 AI 给的正则还是自己写的,都要准备几个应该匹配和不应该匹配的例子,在正则表达式测试器里确认之后再用。

match 找不到时返回 null
const memo = "订单3件,退货12件";
console.log(memo.match(/\d+/g));
console.log("没有数字".match(/\d+/g));
console.log(/\d/.test(memo));
输出
[ '3', '12' ]
null
true
把空白块合成一个,把数字遮住
const messy = "  苹果   梨  橘子 ";
const clean = messy.trim().replace(/\s+/g, " ");
console.log("[" + clean + "]");
const masked = "138-1234-5678".replace(/\d/g, "*");
console.log(masked);
输出
[苹果 梨 橘子]
***-****-****
常见的正则片段
片段含义例子
\d一个数字/\d+/g → 所有连在一起的数字
\s一个空白字符(含制表符、换行)/\s+/g → 所有连续的空白
+前面的内容出现一次或多次/a+/ → a、aa、aaa
^、$字符串的开头、结尾/^\d+$/ → 只有全是数字时
g全部查找(不加则只找第一个)s.replace(/\d/g, "*")

📌 要点总结

  • 字符串是不能修改的值,所有方法都返回新字符串——必须用变量接住结果
  • slice(开始, 结束) 不包含结束位置,indexOf 找不到时是 -1——只判断有没有时用 includes
  • “拆分(split)→ 当数组处理 → 拼接(join)”是文本处理的基本流程,拆分空字符串得到 [ '' ]
  • replace 只替换第一个,replaceAll 全部替换
  • length 是 UTF-16 单元数——常用汉字一个字是1,但表情符号是2以上,扩展区生僻字是2,UTF-8 字节数又是另一回事

✍️ 练习题

请先自己作答,再展开“答案与解析”。

Q1. const s = "abc"; s.toUpperCase(); console.log(s); 的输出是?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① abc

toUpperCase() 只是返回新字符串 "ABC",并不改变 s。要像 s = s.toUpperCase() 这样接住结果。

Q2. "138-1234-5678".slice(-4) 的值是?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① "5678"

负数从后往前数。从 -4 到末尾,也就是最后四个字 "5678"。

Q3. if (s.indexOf("甲")) { ... } 在什么情况下会和预期不一样?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① "甲" 在最前面时

在最前面时结果是 0,而 0 是假值,条件就成了假。反过来,找不到时的 -1 是真值,条件反而为真。应该用 includes,或与 !== -1 比较。

Q4. "苹果,梨,苹果".replace("苹果", "橘子") 的结果是?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① "橘子,梨,苹果"

给 replace 传字符串时,只替换第一次出现的那一处。要全部替换就用 replaceAll。

Q5. "汉字".length 是2,"😀".length 也是2,请用一句话说明原因。

答案与解析
参考答案 length 数的不是看到的字数,而是 UTF-16 编码单元的个数;常用汉字一个字占一个单元,而这个表情符号占两个单元。

[..."😀"].length 是1。由几块拼成的表情符号即使这样展开也可能是2以上,要按看到的图形计数,需要 Intl.Segmenter 这类工具。

🧪 代码练习

动手写截取、计数、整理文本的函数。空字符串之类的边界值测试也会一起评分。

代码只在本浏览器内的隔离环境中运行,不会发送到服务器。无法访问网络,超过2秒会自动停止。改过的代码只保存在此浏览器中。按 Ctrl+Enter(⌘+Enter)运行,Tab 键输入两个空格(先按 Esc 再按 Tab 可跳到下一项)。

浏览器脚本未开启,无法运行代码,但题目、初始代码、自动检查项和参考答案都可以直接阅读。

1隐藏姓名

写一个函数 maskName(name):接收姓名 name,只保留第一个字,其余的字全部换成 * 后返回。例:"王小明" → "王**"。单字姓名原样返回,空字符串返回空字符串。

自动检查
  • maskName("王小明")期望值 "王**"
  • maskName("欧阳娜娜")期望值 "欧***"
  • maskName("李")期望值 "李"
  • maskName("")期望值 ""
💡 提示

空字符串时 name.length - 1 是 -1,而 "*".repeat(-1) 会抛出 RangeError。用 Math.max(…, 0) 或条件语句挡住。

查看参考答案
function maskName(name) {
  const first = name.slice(0, 1);
  const rest = name.length - 1;
  return first + "*".repeat(Math.max(rest, 0));
}

2一个词出现了几次

写一个函数 countWord(text, word):统计句子 text 中词 word 出现了几次,返回数字。区分大小写。例:countWord("苹果 梨 苹果", "苹果") → 2。text 为空字符串时是 0。藏在别的词里面也算("苹果树" 里的 "苹果" 也算1次)。假设 word 不是空字符串。

自动检查
  • countWord("苹果 梨 苹果", "苹果")期望值 2
  • countWord("苹果 梨 苹果", "橘子")期望值 0
  • countWord("a-a-a-a", "a")期望值 4
  • countWord("Apple apple", "apple")期望值 1
  • countWord("", "苹果")期望值 0
💡 提示

"a-a".split("a") 会得到 [ '', '-', '' ],首尾多出空块,但块数仍然是“出现次数 + 1”。

查看参考答案
function countWord(text, word) {
  return text.split(word).length - 1;
}

3整理空格

写一个函数 cleanSpaces(s):去掉字符串 s 首尾的空白,并把中间连续的多个空白缩成一个空格后返回。例:" 苹果 梨 " → "苹果 梨"。只有空白的字符串变成空字符串。

自动检查
  • cleanSpaces(" 苹果 梨 ")期望值 "苹果 梨"
  • cleanSpaces("a b c")期望值 "a b c"
  • cleanSpaces("已经 很整齐")期望值 "已经 很整齐"
  • cleanSpaces(" ")期望值 ""
  • cleanSpaces("")期望值 ""
💡 提示

也可以用 split(/\s+/) 拆开再用 join(" ") 拼起来。不过那样也要先 trim(),首尾才不会多出空块。

查看参考答案
function cleanSpaces(s) {
  return s.trim().replace(/\s+/g, " ");
}

4把标题变成网址用的名字

写一个函数 toSlug(title):接收英文标题 title,转换成适合放进网址的名字(slug)。规则:去掉首尾空白 → 全部转小写 → 连续空白换成一个连字符 -。例:" Hello World " → "hello-world"。

自动检查
  • toSlug(" Hello World ")期望值 "hello-world"
  • toSlug("My First Post")期望值 "my-first-post"
  • toSlug("JS")期望值 "js"
  • toSlug("a b c d")期望值 "a-b-c-d"
  • toSlug("")期望值 ""
💡 提示

初始代码里的 replace(" ", "-") 只替换第一个空格。方法可以用点(.)接连调用,顺序很重要。

查看参考答案
function toSlug(title) {
  return title
    .trim()
    .toLowerCase()
    .replace(/\s+/g, "-");
}

🤖 可以这样问 AI

复制后把 [ ] 部分换成你的情况。不要直接相信回答,请与正文对照核实。

想写整理文本的代码时

我想用 JavaScript 写一个整理字符串的函数。输入示例:[2到3个输入示例]。想要的结果:[每个示例的预期结果]。如果不是非用正则不可,请用 `split`、`replaceAll`、`trim` 这类基本方法来写,并告诉我输入为空字符串或只有空格时会怎样。

看不懂 AI 给的正则表达式时

请把下面的正则表达式拆成一段一段,解释每段的含义:[正则表达式]。再举出3个能匹配的例子,和3个看起来能匹配但其实不匹配的例子。我想找的是[想查找的形状],如果这个正则在某些情况下和我的目的不一致,请指出来。

字数和预想的不一样时

在 JavaScript 里,[字符串示例] 的 `length` 是 [得到的值],但屏幕上看到的字数是 [看到的字数]。请从 UTF-16、码点、字节的角度解释为什么不同,并告诉我当我要数的是 [字数/字节数中的哪一种] 时可以用什么方法。

🧰 相关工具

可以当场检查、整理本讲涉及的 JSON、正则表达式和文本的工具。请不要粘贴真实的个人信息或密码。

参考
  • MDN Web Docs — String(JavaScript 参考)
  • MDN Web Docs — 正则表达式指南
  • MDN Web Docs — String.prototype.normalize()、Intl.Segmenter
  • ECMAScript 语言规范(ECMA-262)— String 对象

达成全部学习目标后,请点击完成。

💻 编程基础

  1. 1什么是程序:把事情拆成顺序、条件和循环的思维
  2. 2值、变量与数据类型:给值贴上名字标签
  3. 3条件语句:根据情况走不同的路
  4. 4循环 — 让同一件事准确地重复多次
  5. 5函数 — 拆成一个个有名字的小机器
  6. 6数组与对象 — 装载和处理数据
  7. 7字符串与文本处理——截取、查找、替换文字
  8. 8调试与读懂错误——把红字变成线索
  9. 9简单的自动化示例——用代码做表格计算和文本整理
  10. 10阅读并检验 AI 写的代码——测试、安全、许可证与个人信息
📚 推荐阅读
📊Using AI for Spreadsheets, and Checking the Formulas→ 💻AI Coding Assistants: Security and Licence Risks→ 💬Practising Conversation in a Foreign Language with AI→ ⚙️Finding Work Tasks Worth Automating with AI→
← AI时代基础学力课程