☰
现代 JavaScript 教程:字符串全解析——引号、特殊字符、查找与 Unicode 内部机制
2026/10/6 2:06:23 网站建设 项目流程
  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

本篇技术指南以 现代 JavaScript 教程(zh.javascript.info) 中 字符串章节 为核心展开,系统梳理 JavaScript 字符串的引号体系、特殊字符、常用 API(查找、截取、大小写、比较)以及 UTF-16 代理对与规范化等底层机制,并结合仓库内配套的实战习题(ucFirst、checkSpam、truncate、extractCurrencyValue)给出可运行、可验证的实现方案。读完本文,你将能熟练处理日常字符串操作,并理解 emoji、生僻汉字等罕见字符在 JavaScript 中的真实存储方式,避免踩到length、代理对和大小写比较等经典陷阱。

字符串基础:文本数据在 JavaScript 中的存储形态

在 JavaScript 中,文本数据以字符串(string)形式存储,单个字符没有单独的类型——'a'和"abc"都是字符串。字符串的内部格式始终是 UTF-16,与页面编码无关。这一点决定了后续所有与length、字符访问和 Unicode 相关的行为(详见"内部,Unicode"一节)。

引号:三种写法与反引号的独特能力

字符串可以包含在单引号、双引号或反引号中:

let single = 'single-quoted'; let double = "double-quoted"; let backticks = `backticks`;

单引号和双引号基本相同;反引号(模板字符串)则提供了两项额外能力:

  1. 通过${…}嵌入任意表达式:
function sum(a, b) { return a + b; } alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.
  1. 字符串可以直接跨行:
let guestList = `Guests: * John * Pete * Mary `; alert(guestList); // 客人清单,多行

而单引号/双引号在源码中直接跨行会报错(Unexpected token ILLEGAL)。原因在于单双引号诞生于语言早期,当时没有考虑多行需求;反引号出现较晚,因此更通用。

反引号还支持"模版函数(tagged templates)"语法func\string`:函数func` 会被自动调用,接收字符串和嵌入式表达式并处理它们,便于把字符串包装进自定义模板或处理函数。该功能非常强大但日常使用较少。

特殊字符与转义

单引号/双引号也可以通过"换行符"\n实现跨行字符串:

let guestList = "Guests:\n * John\n * Pete\n * Mary"; alert(guestList); // 一个多行的客人列表

下面两行描述的是同一个字符串:

let str1 = "Hello\nWorld"; // 使用"换行符"创建的两行字符串 // 使用反引号和普通的换行创建的两行字符串 let str2 = `Hello World`; alert(str1 == str2); // true

特殊字符完整清单

所有特殊字符都以反斜杠\开始,\因此被称为"转义字符":

字符描述
\n换行
\r在 Windows 文本文件中,两个字符\r\n的组合代表一个换行;非 Windows 系统上就是\n。这是历史原因造成的,大多数 Windows 软件也理解\n
\',\"引号
\\反斜线
\t制表符
\b,\f,\v退格、换页、垂直标签——为了兼容性保留,现已不使用
\xXX具有给定十六进制 UnicodeXX的字符,如'\x7A'与'z'相同
\uXXXX以 UTF-16 编码的十六进制代码XXXX的字符,如\u00A9是版权符号©,必须正好 4 个十六进制数字
\u{X…XXXXXX}(1 到 6 个十六进制字符)具有给定 UTF-32 编码的 Unicode 符号,可插入由两个 Unicode 符号编码、占用 4 字节的罕见字符

Unicode 转义示例:

alert( "\u00A9" ); // © alert( "\u{20331}" ); // 佫,罕见的中国象形文字(长 Unicode) alert( "\u{1F60D}" ); // 😍,笑脸符号(另一个长 Unicode)

引号的转义与反斜杠的真实去向

在字符串内部插入与外部闭合相同的引号时,需要在其前加反斜杠,否则会被当作字符串结束:

alert( 'I\'m the Walrus!' ); // I'm the Walrus!

只有与外部引号相同的引号才需要转义,因此更优雅的做法是换用不同引号:

alert( `I'm the Walrus!` ); // I'm the Walrus!

注意:反斜杠\只用于源码层面"正确读取"字符串,内存中的字符串并不包含\。若需要显示真实反斜杠,须书写两次\\:

alert( `The backslash: \\` ); // The backslash: \

字符串长度:length是属性而非方法

length属性表示字符串长度:

alert( `My\n`.length ); // 3

这里\n是一个单独的"特殊"字符,所以长度为3。

⚠️length是一个属性掌握其他编程语言的人有时会误写str.length()——这行不通。str.length是数字属性而非函数,后面不需要括号。

访问字符:[]、charAt与for..of

获取pos位置的字符,可用方括号[pos]或str.charAt(pos),第一个字符从零位置开始:

let str = `Hello`; // 第一个字符 alert( str[0] ); // H alert( str.charAt(0) ); // H // 最后一个字符 alert( str[str.length - 1] ); // o

方括号是现代的写法,charAt因历史原因存在。二者唯一区别是:未找到字符时,[]返回undefined,charAt返回空字符串:

let str = `Hello`; alert( str[1000] ); // undefined alert( str.charAt(1000) ); // ''(空字符串)

也可以使用for..of遍历字符:

for (let char of "Hello") { alert(char); // H,e,l,l,o }

字符串不可变:替换必须重建新字符串

JavaScript 中字符串不可更改,改变单个字符是不可能的:

let str = 'Hi'; str[0] = 'h'; // error(严格模式下直接报错) alert( str[0] ); // 无法运行

通常的解决方法是创建新字符串再赋回原变量:

let str = 'Hi'; str = 'h' + str[1]; // 替换字符串 alert( str ); // hi

这一约束也直接影响了下面的"首字母大写"习题解法(见"实战:配套任务")。

改变大小写

toLowerCase()与toUpperCase()改变字符串大小写:

alert( 'Interface'.toUpperCase() ); // INTERFACE alert( 'Interface'.toLowerCase() ); // interface

也常与字符访问组合,实现"只转换某个字符":

alert( 'Interface'[0].toLowerCase() ); // 'i'

查找子字符串

str.indexOf(substr, pos)

str.indexOf(substr, pos)从位置pos开始查找substr:找不到返回-1,否则返回匹配位置。检索大小写敏感:

let str = 'Widget with id'; alert( str.indexOf('Widget') ); // 0,一开始就被找到 alert( str.indexOf('widget') ); // -1,没有找到,大小写敏感 alert( str.indexOf("id") ); // 1,"id" 在位置 1 处

可选的第二参数支持从给定位置继续查找:

let str = 'Widget with id'; alert( str.indexOf('id', 2) ) // 12

用循环找出所有匹配位置——每次新调用都从上一个匹配位置之后开始:

let str = 'As sly as a fox, as strong as an ox'; let target = 'as'; // 要查找的目标 let pos = 0; while (true) { let foundPos = str.indexOf(target, pos); if (foundPos == -1) break; alert( `Found at ${foundPos}` ); pos = foundPos + 1; // 继续从下一个位置查找 }

同样的算法可以简写:

let str = "As sly as a fox, as strong as an ox"; let target = "as"; let pos = -1; while ((pos = str.indexOf(target, pos + 1)) != -1) { alert( pos ); }

💡str.lastIndexOf(substr, pos)类似的方法lastIndexOf从字符串末尾向开头搜索,会以相反顺序列出匹配位置。

经典的if陷阱与按位 NOT 技巧

indexOf在if测试中有一个经典陷阱:它返回的是位置而非布尔值。

let str = "Widget with id"; if (str.indexOf("Widget")) { alert("We found it"); // 不工作! }

因为str.indexOf("Widget")返回0(在起始位置就命中),而if把0视为false。因此必须显式与-1比较:

let str = "Widget with id"; if (str.indexOf("Widget") != -1) { alert("We found it"); // 现在工作了! }

旧代码中常见"按位 NOT"技巧:~将数字转为 32-bit 整数并逐位取反,对 32-bit 整数而言~n === -(n+1):

alert( ~2 ); // -3,和 -(2+1) 相同 alert( ~1 ); // -2,和 -(1+1) 相同 alert( ~0 ); // -1,和 -(0+1) 相同 alert( ~-1 ); // 0,和 -(-1+1) 相同

只有当n == -1时~n才为零,因此if (~str.indexOf("..."))等价于"找到了":

let str = "Widget"; if (~str.indexOf("Widget")) { alert( 'Found it!' ); // 正常运行 }

需注意:由于~会把大数字截断为 32 位,其他数字也可能产生0(最小的是~4294967295 === 0),所以该技巧仅在字符串不太长时可靠。如今它只出现在旧代码里,现代写法用.includes即可。

includes、startsWith、endsWith

更现代的方法str.includes(substr, pos)只关心"是否包含",返回true/false:

alert( "Widget with id".includes("Widget") ); // true alert( "Hello".includes("Bye") ); // false

第二参数是起始搜索位置:

alert( "Widget".includes("id") ); // true alert( "Widget".includes("id", 3) ); // false, 从位置 3 开始没有 "id"

str.startsWith与str.endsWith的功能与名称一致:

alert( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始 alert( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 结束

获取子字符串:slice、substring、substr

三种方法对比:

方法选择方式……负值参数
slice(start, end)从start到end(不含end)允许
substring(start, end)从start到end(不含end)负值被视为0
substr(start, length)从start开始获取长为length的字符串允许start为负数

str.slice(start [, end])返回从start到(不含)end的部分:

let str = "stringify"; alert( str.slice(0, 5) ); // 'strin',从 0 到 5(不含 5) alert( str.slice(0, 1) ); // 's',只有 0 处的字符

省略第二参数则取到末尾;start/end可为负值,表示从字符串结尾倒着数:

let str = "stringify"; alert( str.slice(2) ); // 从第二个位置直到结束 // 从右边的第四个位置开始,在右边的第一个位置结束 alert( str.slice(-4, -1) ); // 'gif'

str.substring(start [, end])与slice几乎相同,但允许start大于end(自动交换):

let str = "stringify"; // 这些对于 substring 是相同的 alert( str.substring(2, 6) ); // "ring" alert( str.substring(6, 2) ); // "ring" // ……但对 slice 是不同的: alert( str.slice(2, 6) ); // "ring"(一样) alert( str.slice(6, 2) ); // ""(空字符串)

substring不支持负参数(视为0)。

str.substr(start [, length])用"长度"取代"结束位置":

let str = "stringify"; alert( str.substr(2, 4) ); // 'ring',从位置 2 开始,获取 4 个字符

第一参数可为负,从结尾算起:

let str = "stringify"; alert( str.substr(-4, 2) ); // 'gi',从第 4 位获取 2 个字符

💡使用哪一个?三种方法都能取子串。正式地说,substr有个小缺点:它不在 JavaScript 核心规范中,而是写在附录 B(主要收录因历史原因遗留的仅浏览器特性)中,因此理论上非浏览器环境可能不支持;实际上它在几乎所有环境中都能用。slice支持负值且写法更简短,记住slice一个就够用了。

比较字符串:字母序、字符代码与localeCompare

字符串按字母顺序逐字符比较(见 比较运算符章节),但有两个反直觉的怪象:

  1. 小写字母总是大于大写字母:
alert( 'a' > 'Z' ); // true
  1. 带变音符号的字母"乱序":
alert( 'Österreich' > 'Zealand' ); // true

如果直接对这些国家名排序,结果会与人们直觉不符(通常人们期望Zealand排在Österreich之后)。要理解原因,需要回到 UTF-16 内部表示:每个字符都有对应的数字代码。

字符与代码互转

str.codePointAt(pos)返回pos位置的字符代码:

alert( "z".codePointAt(0) ); // 122 alert( "Z".codePointAt(0) ); // 90

String.fromCodePoint(code)由数字代码创建字符:

alert( String.fromCodePoint(90) ); // Z

也可以用\u加十六进制代码实现同样效果(十六进制中 90 写作5a):

alert( '\u005a' ); // Z

把代码65..220的字符全部拼出来,可以直观看到排序真相:

let str = ''; for (let i = 65; i <= 220; i++) { str += String.fromCodePoint(i); } alert( str ); // ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~… // ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ

先是大写字符,然后是一些特殊字符,再是小写字符,Ö几乎排在最后。字符通过数字代码比较:a(97)大于Z(90),所以'a' > 'Z';Ö的代码比所有a..z都大,所以'Österreich' > 'Zealand'。

正确的比较:str.localeCompare(str2)

"正确"的字符串比较算法远比逐代码比较复杂,因为不同语言的字母各不相同,浏览器需要知道要比较的语言。所有现代浏览器都支持国际化标准 ECMA-402(IE10 及以下需要额外引入 Intl.js 兼容库),它提供str.localeCompare(str2)按语言规则返回整数:

  • str排在str2前面→ 返回负数;
  • str排在str2后面→ 返回正数;
  • 二者相同位置 → 返回0。
alert( 'Österreich'.localeCompare('Zealand') ); // -1

localeCompare还支持两个额外参数,用于指定语言(默认从运行环境获取,字符顺序视语言而异)并设置附加规则,如区分大小写、是否把"a"与"á"视为相同等。

内部,Unicode:代理对与规范化

⚠️ 本节是进阶内容。如果你要处理 emoji、罕见的数学符号、生僻汉字或其他稀有符号,这部分知识非常有用;否则可以跳过。

代理对(Surrogate Pairs)

所有常用字符都是 2 字节代码——欧洲语言字母、数字甚至大多数象形文字都如此。但 2 字节只有 65536 种组合,不足以覆盖每个可能的符号。因此稀有符号用"代理对"编码:一对 2 字节符号。

代理对符号的length是2:

alert( '𝒳'.length ); // 2,大写数学符号 X alert( '😂'.length ); // 2,笑哭表情 alert( '𩷶'.length ); // 2,罕见的中国象形文字

代理对在 JavaScript 诞生时并不存在,因此语言本身无法正确处理:上面每个字符串都只有一个"符号",但length显示为2。

String.fromCodePoint和str.codePointAt是少数能正确处理代理对的方法,它们出现得较晚;更早的String.fromCharCode与str.charCodeAt与它们类似,但不理解代理对。

通过下标访问会拿到"半个符号",毫无意义:

alert( '𝒳'[0] ); // 奇怪的符号…… alert( '𝒳'[1] ); // ……代理对的一块

从技术角度,代理对可通过代码范围检测:第一部分的代码在0xd800..0xdbff之间,第二部分必须在0xdc00..0xdfff之间,这些范围是标准专门为代理对保留的:

// charCodeAt 不理解代理对,所以给出的是代理对半边的代码 alert( '𝒳'.charCodeAt(0).toString(16) ); // d835,在 0xd800 和 0xdbff 之间 alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, 在 0xdc00 和 0xdfff 之间

处理代理对的更多方法见 可迭代对象章节。

变音符号与规范化

许多语言中存在"基础字符 + 上方/下方标记"的组合符号,如a可派生出àáâäãåā。常见组合在 UTF-16 中都有自己的代码,但组合太多不可能全覆盖。为此 UTF-16 允许用多个 Unicode 字符表示:基础字符后紧跟一个或多个"标记"字符。

例如S后跟"上方加点"字符(代码\u0307)显示为Ṡ:

alert( 'S\u0307' ); // Ṡ

再追加"下方加点"字符(代码\u0323),得到"上下都有点"的Ṩ:

alert( 'S\u0307\u0323' ); // Ṩ

这带来灵活性的同时产生一个有趣问题:两个视觉完全相同的字符可能由不同的 Unicode 组合表示:

let s1 = 'S\u0307\u0323'; // Ṩ,S + 上点 + 下点 let s2 = 'S\u0323\u0307'; // Ṩ,S + 下点 + 上点 alert( `s1: ${s1}, s2: ${s2}` ); alert( s1 == s2 ); // false,尽管字符看起来相同(?!)

解决方案是"Unicode 规范化"算法,由str.normalize()实现,把每个字符串转为统一的"通用"格式:

alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true

有趣的是,实际规范化会把 3 个字符的序列合并为单个代码\u1e68(S 带上下两个点):

alert( "S\u0307\u0323".normalize().length ); // 1 alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true

(并不是所有组合都会合并——Ṩ是"常用"符号,因此被 UTF-16 主表收录并分配了代码。规范化的完整规则与变体参见 Unicode 标准附录的"Unicode 规范化形式",对大多数实际用途而言,本文内容已经足够。)

实战:仓库配套任务与可运行解答

教程为本文主题配套了四个实战任务,均位于 1-js/05-data-types/03-string 目录下,每个任务都附有测试(_js.view/test.js)与解答(_js.view/solution.js),可直接对照验证。

任务一:首字母大写ucFirst

任务描述见 1-ucfirst/task.md:写函数ucFirst(str)返回首字母大写的字符串,如ucFirst("john") == "John"。

解法:不能直接"替换"首字符(字符串不可变),应基于原字符串构造新串。但需当心空字符串:str[0]为undefined,没有toUpperCase()方法会报错。可改用str.charAt(0)(永远返回字符串,可能为空),或显式做空串判断。仓库标准解答采用后者:

function ucFirst(str) { if (!str) return str; return str[0].toUpperCase() + str.slice(1); }

其配套测试见 1-ucfirst/_js.view/test.js,覆盖了"首字符大写"与"空串不报错"两个用例(assert.strictEqual(ucFirst("john"), "John")与assert.strictEqual(ucFirst(""), ""))。

任务二:检查 spamcheckSpam

任务描述见 2-check-spam/task.md:若str包含viagra或XXX(不区分大小写)返回true,否则false。

解法:先toLowerCase()统一为小写,再用includes做不区分大小写的检测:

function checkSpam(str) { let lowerStr = str.toLowerCase(); return lowerStr.includes('viagra') || lowerStr.includes('xxx'); } alert( checkSpam('buy ViAgRA now') ); // true alert( checkSpam('free xxxxx') ); // true alert( checkSpam("innocent rabbit") ); // false

这正体现了includes与indexOf的选型差异:只关心是否命中、不需要位置时,includes是更现代的直白选择。

任务三:截断文本truncate

任务描述见 3-truncate/task.md:若str超过maxlength,用"…"替换结尾,总长度仍等于maxlength。

解法:为给省略号留空间,截取长度应为maxlength - 1;注意省略号…是单个 Unicode 字符而非三个点:

function truncate(str, maxlength) { return (str.length > maxlength) ? str.slice(0, maxlength - 1) + '…' : str; }

验证:

truncate("What I'd like to tell on this topic is:", 20) // "What I'd like to te…" truncate("Hi everyone!", 20) // "Hi everyone!"

任务四:提取货币数值extractCurrencyValue

任务描述见 4-extract-currency/task.md:输入形如"$120"的字符串,提取其中的数值并返回,如extractCurrencyValue('$120') === 120。

思路:先用slice去掉开头的货币符号(如str.slice(1)),再通过数值转换(如+、Number()或parseFloat)得到数字。该任务是对"取子串 + 类型转换"的综合练习,相关数值转换细节可参考 类型转换章节。

总结

  • 有 3 种引号:反引号支持跨行与${…}嵌入表达式;
  • JavaScript 字符串内部使用 UTF-16 编码;
  • 可用\n等特殊字符或\u...按 Unicode 代码插入字符;
  • 取字符用[];取子串用slice或substring;
  • 大小写转换用toLowerCase/toUpperCase;
  • 查找子串用indexOf,简单存在性检查用includes/startsWith/endsWith;
  • 按语言比较字符串用localeCompare,否则按字符代码比较;
  • 字符串不可变,修改需重建新字符串;
  • 代理对使 emoji/生僻字等length为 2,处理此类符号要用codePointAt/fromCodePoint与normalize()。

此外还有几个常用的字符串方法:

  • str.trim()—— 删除字符串首尾空白;
  • str.repeat(n)—— 重复字符串n次;
  • 更多方法参见 String 手册。

字符串还支持基于正则表达式的搜索/替换方法,这个话题较大,教程在 正则表达式章节 中单独展开讨论。

  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

相关推荐

上一篇:SAN动画与过渡效果:transition指令的完整使用手册
下一篇:Rusted PackFile Manager:Total War系列游戏的终极MOD开发工具

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询