资讯详情 现代 JavaScript 教程:字符串全解析——引号、特殊字符、查找与 Unicode 内部机制
📅 2026/10/6 2:06:26
文档教程前端【免费下载链接】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;单引号和双引号基本相同反引号模板字符串则提供了两项额外能力通过${…}嵌入任意表达式function sum(a, b) { return a b; } alert(1 2 ${sum(1, 2)}.); // 1 2 3.字符串可以直接跨行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! ); // Im the Walrus!只有与外部引号相同的引号才需要转义因此更优雅的做法是换用不同引号alert( Im the Walrus! ); // Im 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因历史原因存在。二者唯一区别是未找到字符时[]返回undefinedcharAt返回空字符串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) ); // 1id 在位置 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 -(n1)alert( ~2 ); // -3和 -(21) 相同 alert( ~1 ); // -2和 -(11) 相同 alert( ~0 ); // -1和 -(01) 相同 alert( ~-1 ); // 0和 -(-11) 相同只有当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/falsealert( Widget with id.includes(Widget) ); // true alert( Hello.includes(Bye) ); // false第二参数是起始搜索位置alert( Widget.includes(id) ); // true alert( Widget.includes(id, 3) ); // false, 从位置 3 开始没有 idstr.startsWith与str.endsWith的功能与名称一致alert( Widget.startsWith(Wid) ); // trueWidget 以 Wid 开始 alert( Widget.endsWith(get) ); // trueWidget 以 get 结束获取子字符串slice、substring、substr三种方法对比方法选择方式……负值参数slice(start, end)从start到end不含end允许substring(start, end)从start到end不含end负值被视为0substr(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) ); // gifstr.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字符串按字母顺序逐字符比较见 比较运算符章节但有两个反直觉的怪象小写字母总是大于大写字母alert( a Z ); // true带变音符号的字母乱序alert( Österreich Zealand ); // true如果直接对这些国家名排序结果会与人们直觉不符通常人们期望Zealand排在Österreich之后。要理解原因需要回到 UTF-16 内部表示每个字符都有对应的数字代码。字符与代码互转str.codePointAt(pos)返回pos位置的字符代码alert( z.codePointAt(0) ); // 122 alert( Z.codePointAt(0) ); // 90String.fromCodePoint(code)由数字代码创建字符alert( String.fromCodePoint(90) ); // Z也可以用\u加十六进制代码实现同样效果十六进制中 90 写作5aalert( \u005a ); // Z把代码65..220的字符全部拼出来可以直观看到排序真相let str ; for (let i 65; i 220; i) { str String.fromCodePoint(i); } alert( str ); // ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}~… // ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ先是大写字符然后是一些特殊字符再是小写字符Ö几乎排在最后。字符通过数字代码比较a97大于Z90所以a ZÖ的代码比所有a..z都大所以Österreich Zealand。正确的比较str.localeCompare(str2)正确的字符串比较算法远比逐代码比较复杂因为不同语言的字母各不相同浏览器需要知道要比较的语言。所有现代浏览器都支持国际化标准 ECMA-402IE10 及以下需要额外引入 Intl.js 兼容库它提供str.localeCompare(str2)按语言规则返回整数str排在str2前面→ 返回负数str排在str2后面→ 返回正数二者相同位置 → 返回0。alert( Österreich.localeCompare(Zealand) ); // -1localeCompare还支持两个额外参数用于指定语言默认从运行环境获取字符顺序视语言而异并设置附加规则如区分大小写、是否把a与á视为相同等。内部Unicode代理对与规范化⚠️ 本节是进阶内容。如果你要处理 emoji、罕见的数学符号、生僻汉字或其他稀有符号这部分知识非常有用否则可以跳过。代理对Surrogate Pairs所有常用字符都是 2 字节代码——欧洲语言字母、数字甚至大多数象形文字都如此。但 2 字节只有 65536 种组合不足以覆盖每个可能的符号。因此稀有符号用代理对编码一对 2 字节符号。代理对符号的length是2alert( .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 个字符的序列合并为单个代码\u1e68S 带上下两个点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 Id like to tell on this topic is:, 20) // What Id 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点击查看免费下载相关推荐JavaScript教程深入理解Unicode与字符串内部机制JavaScript教程深入理解Unicode与字符串内部机制 前言为什么需要了解Unicode 在JavaScript中字符串是基于Unicode标准构文档/教程前端5个颠覆性功能为什么Open Interpreter是本地AI编程的终极选择5个颠覆性功能为什么Open Interpreter是本地AI编程的终极选择 在AI编程助手百花齐放的今天你可能会感到困惑为什么还要选择另一个工具让我告人工智能大模型AI Agent代码智能体AI 应用CLIVim字符编码表vim-galore教你查找与输入特殊符号Vim字符编码表vim galore教你查找与输入特殊符号 在日常文本编辑中特殊符号的输入和查找常常让Vim用户感到困扰。无论是编写技术文档需要插入特殊符号文档教程开发工具上一篇SAN动画与过渡效果transition指令的完整使用手册下一篇Rusted PackFile ManagerTotal War系列游戏的终极MOD开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考