- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
本篇技术指南以 现代 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!' ); // 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
字符串按字母顺序逐字符比较(见 比较运算符章节),但有两个反直觉的怪象:
- 小写字母总是大于大写字母:
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 写作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') ); // -1localeCompare还支持两个额外参数,用于指定语言(默认从运行环境获取,字符顺序视语言而异)并设置附加规则,如区分大小写、是否把"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 相关知识。
相关推荐
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用户感到困扰。无论是编写技术文档需要插入特殊符号
文档教程开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考