1.字符串是一串字符
第2讲说过,字符串(string)就是用引号括起来的值。双引号 "苹果" 和单引号 '苹果' 是同一个字符串;用反引号括起来的模板字符串(模板字面量)则用来把变量的值嵌进字符串里。模板字符串长什么样,请直接看下面的代码。不管用哪种引号,在同一个程序里统一成一种会更好读。
字符串是按顺序排列的一串字符,所以可以像第6讲的数组一样,用编号(索引)逐个取出字符。第一个字符是0号,s.length 是以字符为单位的长度。因此最后一个字符是 s[s.length - 1],现在也常用 s.at(-1) 这种从后往前数的写法。取一个不存在的编号时不会报错,而是得到 undefined,这一点也和数组一样。
字符串和数组有一个很大的不同:字符串一旦创建,里面的字符就不能修改。如果像 s[0] = "z" 这样想改其中一个字,在严格模式下运行的代码(比如这个练习场)里会出现 TypeError。所以本讲的所有方法都不会改动原字符串,而是返回一个“新字符串”。要使用结果,就必须用变量接住它。
undefinedconst 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
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 字节 |
|---|---|---|---|
"汉字" | 2 | 2 | 6 |
"abc" | 3 | 3 | 3 |
"😀" | 2 | 1 | 4 |
"👍🏽" | 4 | 2 | 8 |
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步 找到
@的位置。indexOf("@")是5(m-i-n-s-u 是0到4号)。 - 第2步 从0号截到5号的“前一个”即可。也就是
slice(0, 5),长度是 5 − 0 = 5个字。 - 第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 找不到时返回 nullconst 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 字节数又是另一回事
🧪 代码练习
动手写截取、计数、整理文本的函数。空字符串之类的边界值测试也会一起评分。
代码只在本浏览器内的隔离环境中运行,不会发送到服务器。无法访问网络,超过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("苹果 梨 苹果", "苹果")期望值2countWord("苹果 梨 苹果", "橘子")期望值0countWord("a-a-a-a", "a")期望值4countWord("Apple apple", "apple")期望值1countWord("", "苹果")期望值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 对象
达成全部学习目标后,请点击完成。
此浏览器无法保存记录,仅在当前页面显示。