2024 年 1 月 17 日

字符串

在 JavaScript 中,文本数据存储为字符串。没有单独的字符类型。

字符串的内部格式始终为 UTF-16,与页面编码无关。

引号

让我们回顾一下引号的类型。

字符串可以用单引号、双引号或反引号引起来

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); // a list of guests, multiple lines

看起来很自然,对吧?但单引号或双引号不起作用。

如果我们使用它们并尝试使用多行,将会出现错误

let guestList = "Guests: // Error: Unexpected token ILLEGAL
  * John";

单引号和双引号来自古代语言创造时期,当时并未考虑多行字符串的需要。反引号出现得晚得多,因此更加通用。

反引号还允许我们在第一个反引号之前指定一个“模板函数”。语法是:func`string`。函数 func 会自动调用,接收字符串和嵌入式表达式,并可以处理它们。此功能称为“标记模板”,很少见,但您可以在 MDN 中阅读有关它的信息:模板文字

特殊字符

仍然可以使用单引号和双引号创建多行字符串,方法是使用所谓的“换行符”,写成 \n,表示换行

let guestList = "Guests:\n * John\n * Pete\n * Mary";

alert(guestList); // a multiline list of guests, same as above

作为一个更简单的示例,这两行是相等的,只是写法不同

let str1 = "Hello\nWorld"; // two lines using a "newline symbol"

// two lines using a normal newline and backticks
let str2 = `Hello
World`;

alert(str1 == str2); // true

还有其他不那么常见的特殊字符

字符 说明
\n 换行
\r 在 Windows 文本文件中,两个字符 \r\n 的组合表示一个新中断,而在非 Windows 操作系统上,它只是 \n。这是出于历史原因,大多数 Windows 软件也理解 \n
\'\"\` 引号
\\ 反斜杠
\t 制表符
\b\f\v 退格、换页符、垂直制表符——为了完整性而提及,来自过去,现在不使用了(你现在就可以忘记它们了)。

如您所见,所有特殊字符都以反斜杠字符 \ 开头。它也称为“转义字符”。

因为它非常特殊,所以如果我们需要在字符串中显示实际的反斜杠 \,我们需要加倍

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

所谓的“转义”引号 \'\"\` 用于将引号插入到同引号的字符串中。

例如

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

如您所见,我们必须用反斜杠 \' 前置内部引号,否则它将表示字符串结束。

当然,只有与包含引号相同的引号才需要转义。所以,作为一种更优雅的解决方案,我们可以改用双引号或反引号

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

除了这些特殊字符,还有一种特殊的 Unicode 代码 \u… 表示法,它很少使用,并在关于 Unicode 的可选章节中进行了介绍。

字符串长度

length 属性具有字符串长度

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

请注意,\n 是一个单独的“特殊”字符,因此长度实际上是 3

length 是一个属性

有时,具有其他语言背景的人会错误地调用 str.length(),而不是仅调用 str.length。这不起作用。

请注意,str.length 是一个数字属性,而不是一个函数。无需在其后添加括号。不是 .length(),而是 .length

访问字符

要获取位置 pos 处的字符,请使用方括号 [pos] 或调用方法 str.at(pos)。第一个字符从零位置开始

let str = `Hello`;

// the first character
alert( str[0] ); // H
alert( str.at(0) ); // H

// the last character
alert( str[str.length - 1] ); // o
alert( str.at(-1) );

如您所见,.at(pos) 方法有一个好处,即允许负位置。如果 pos 为负,则它将从字符串末尾开始计数。

因此,.at(-1) 表示最后一个字符,.at(-2) 表示其前一个字符,依此类推。

方括号始终为负索引返回 undefined,例如

let str = `Hello`;

alert( str[-2] ); // undefined
alert( str.at(-2) ); // l

我们还可以使用 for..of 迭代字符

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

字符串是不可变的

在 JavaScript 中无法更改字符串。不可能更改字符。

让我们尝试一下,以表明它不起作用

let str = 'Hi';

str[0] = 'h'; // error
alert( str[0] ); // doesn't work

通常的解决方法是创建一个全新的字符串,并将其分配给 str,而不是旧字符串。

例如

let str = 'Hi';

str = 'h' + str[1]; // replace the string

alert( str ); // hi

在以下部分中,我们将看到更多此类示例。

更改大小写

方法 toLowerCase()toUpperCase() 更改大小写

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

或者,如果我们希望将单个字符小写

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

搜索子字符串

有多种方法可以在字符串中查找子字符串。

str.indexOf

第一种方法是 str.indexOf(substr, pos)

它从给定的位置 pos 开始在 str 中查找 substr,并返回找到匹配项的位置或在找不到任何内容时返回 -1

例如

let str = 'Widget with id';

alert( str.indexOf('Widget') ); // 0, because 'Widget' is found at the beginning
alert( str.indexOf('widget') ); // -1, not found, the search is case-sensitive

alert( str.indexOf("id") ); // 1, "id" is found at the position 1 (..idget with id)

可选的第二个参数允许我们从给定位置开始搜索。

例如,"id" 的第一个出现位置在 1。要查找下一个出现位置,让我们从位置 2 开始搜索

let str = 'Widget with id';

alert( str.indexOf('id', 2) ) // 12

如果我们对所有出现位置感兴趣,我们可以循环运行 indexOf。每次新调用都会使用上一次匹配后的位置

let str = 'As sly as a fox, as strong as an ox';

let target = 'as'; // let's look for it

let pos = 0;
while (true) {
  let foundPos = str.indexOf(target, pos);
  if (foundPos == -1) break;

  alert( `Found at ${foundPos}` );
  pos = foundPos + 1; // continue the search from the next position
}

相同的算法可以简短地表述为

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, position)

还有一个类似的方法 str.lastIndexOf(substr, position),它从字符串的末尾向开头搜索。

它会按相反的顺序列出出现位置。

if 测试中的 indexOf 存在一个轻微的不便。我们不能像这样将其放入 if

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("We found it"); // doesn't work!
}

上面的示例中的 alert 不会显示,因为 str.indexOf("Widget") 返回 0(表示它在起始位置找到了匹配项)。没错,但 if 认为 0false

所以,我们应该实际检查 -1,如下所示

let str = "Widget with id";

if (str.indexOf("Widget") != -1) {
    alert("We found it"); // works now!
}

includes、startsWith、endsWith

更现代的方法 str.includes(substr, pos) 根据 str 是否包含 substr 返回 true/false

如果我们需要测试匹配项,但不需要其位置,那么这是正确的选择

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

alert( "Hello".includes("Bye") ); // false

str.includes 的可选第二个参数是从其开始搜索的位置

alert( "Widget".includes("id") ); // true
alert( "Widget".includes("id", 3) ); // false, from position 3 there is no "id"

方法 str.startsWithstr.endsWith 完全按照它们所说的那样做

alert( "Widget".startsWith("Wid") ); // true, "Widget" starts with "Wid"
alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get"

获取子字符串

JavaScript 中有 3 种获取子字符串的方法:substringsubstrslice

str.slice(start [, end])

返回从 start 到(但不包括)end 的字符串部分。

例如

let str = "stringify";
alert( str.slice(0, 5) ); // 'strin', the substring from 0 to 5 (not including 5)
alert( str.slice(0, 1) ); // 's', from 0 to 1, but not including 1, so only character at 0

如果没有第二个参数,则 slice 会一直到字符串末尾

let str = "stringify";
alert( str.slice(2) ); // 'ringify', from the 2nd position till the end

start/end 的负值也是可能的。它们表示位置是从字符串末尾开始计数的

let str = "stringify";

// start at the 4th position from the right, end at the 1st from the right
alert( str.slice(-4, -1) ); // 'gif'
str.substring(start [, end])

返回字符串中介于 startend 之间的部分(不包括 end)。

这与 slice 几乎相同,但它允许 start 大于 end(在这种情况下,它只是交换 startend 的值)。

例如

let str = "stringify";

// these are same for substring
alert( str.substring(2, 6) ); // "ring"
alert( str.substring(6, 2) ); // "ring"

// ...but not for slice:
alert( str.slice(2, 6) ); // "ring" (the same)
alert( str.slice(6, 2) ); // "" (an empty string)

不支持负参数(与 slice 不同),它们被视为 0

str.substr(start [, length])

返回从 start 开始,长度为 length 的字符串部分。

与前一种方法不同,此方法允许我们指定 length,而不是结束位置。

let str = "stringify";
alert( str.substr(2, 4) ); // 'ring', from the 2nd position get 4 characters

第一个参数可以是负数,以从末尾开始计数。

let str = "stringify";
alert( str.substr(-4, 2) ); // 'gi', from the 4th position get 2 characters

此方法位于语言规范的 附件 B 中。这意味着只有浏览器托管的 Javascript 引擎应该支持它,不建议使用它。在实践中,它得到了广泛的支持。

让我们回顾一下这些方法,以避免混淆。

方法 选择... 负值
slice(start, end) startend(不包括 end 允许负值
substring(start, end) 介于 startend 之间(不包括 end 负值表示 0
substr(start, length) start 获取 length 个字符 允许负 start
选择哪一个?

所有这些都可以完成这项工作。从形式上讲,substr 有一个小的缺点:它不是在核心 JavaScript 规范中描述的,而是在附件 B 中描述的,该附件涵盖了主要出于历史原因而存在的仅浏览器功能。因此,非浏览器环境可能无法支持它。但在实践中,它可以在任何地方工作。

在其他两个变体中,slice 稍微灵活一些,它允许负参数,并且书写更短。

因此,对于实际使用,记住 slice 就足够了。

比较字符串

正如我们在 比较 一章中所知,字符串按字母顺序逐个字符进行比较。

不过,有一些奇怪之处。

  1. 小写字母始终大于大写字母

    alert( 'a' > 'Z' ); // true
  2. 带变音符号的字母“乱序”

    alert( 'Österreich' > 'Zealand' ); // true

    如果我们对这些国家名称进行排序,这可能会导致奇怪的结果。通常人们会希望 Zealand 在列表中排在 Österreich 之后。

要理解发生了什么,我们应该知道 Javascript 中的字符串使用 UTF-16 编码。也就是说:每个字符都有一个相应的数字代码。

有特殊方法可以获取代码的字符,反之亦然

str.codePointAt(pos)

返回一个十进制数字,表示位置pos处字符的代码

// different case letters have different codes
alert( "Z".codePointAt(0) ); // 90
alert( "z".codePointAt(0) ); // 122
alert( "z".codePointAt(0).toString(16) ); // 7a (if we need a hexadecimal value)
String.fromCodePoint(code)

通过数字code创建字符

alert( String.fromCodePoint(90) ); // Z
alert( String.fromCodePoint(0x5a) ); // Z (we can also use a hex value as an argument)

现在让我们通过制作一个字符串来查看代码为65..220(拉丁字母和一些额外字母)的字符

let str = '';

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

看到了吗?大写字符排在前面,然后是一些特殊字符,然后是小写字符,最后输出中接近末尾的是Ö

现在很明显为什么a > Z

字符按其数字代码进行比较。代码越大,表示字符越大。a (97) 的代码大于Z (90) 的代码。

  • 所有小写字母都排在大写字母之后,因为它们代码更大。
  • 一些字母(如Ö)与主字母表分开。此处,其代码大于从az的任何字符。

正确的比较

执行字符串比较的“正确”算法比看起来要复杂,因为不同语言的字母表不同。

因此,浏览器需要知道要比较的语言。

幸运的是,现代浏览器支持国际化标准ECMA-402

它提供了一种特殊方法,用于按照不同语言的规则比较字符串。

调用str.localeCompare(str2)返回一个整数,表示str根据语言规则小于、等于或大于str2

  • 如果str小于str2,则返回负数。
  • 如果str大于str2,则返回正数。
  • 如果它们相等,则返回0

例如

alert( 'Österreich'.localeCompare('Zealand') ); // -1

此方法实际上还有两个附加参数,在文档中指定,它允许指定语言(默认情况下从环境中获取,字母顺序取决于语言)并设置附加规则,例如区分大小写或应将"a""á"视为相同等。

总结

  • 有 3 种类型的引号。反引号允许字符串跨越多行并嵌入表达式${…}
  • 我们可以使用特殊字符,比如换行符 \n
  • 要获取一个字符,使用:[]at 方法。
  • 要获取一个子字符串,使用:slicesubstring
  • 要将字符串转为小写/大写,使用:toLowerCase/toUpperCase
  • 要查找一个子字符串,使用:indexOf,或 includes/startsWith/endsWith 进行简单检查。
  • 要根据语言比较字符串,使用:localeCompare,否则它们将按字符代码比较。

字符串中还有其他一些有用的方法

  • str.trim() – 从字符串的开头和结尾移除(“修剪”)空格。
  • str.repeat(n) – 重复字符串 n 次。
  • …更多内容可在 手册 中找到。

字符串还具有使用正则表达式进行搜索/替换的方法。但这是一个很大的主题,因此在单独的教程部分 正则表达式 中进行了说明。

此外,现在重要的是要知道字符串基于 Unicode 编码,因此比较存在问题。在 Unicode、字符串内部 一章中有更多关于 Unicode 的内容。

任务

重要性:5

编写一个函数 ucFirst(str),它返回字符串 str,第一个字符大写,例如

ucFirst("john") == "John";

打开一个带有测试的沙箱。

我们无法“替换”第一个字符,因为 JavaScript 中的字符串是不可变的。

但我们可以基于现有字符串创建一个新字符串,第一个字符大写

let newStr = str[0].toUpperCase() + str.slice(1);

不过有一个小问题。如果 str 为空,则 str[0]undefined,并且由于 undefined 没有 toUpperCase() 方法,因此我们会收到一个错误。

最简单的解决方法是添加一个空字符串测试,如下所示

function ucFirst(str) {
  if (!str) return str;

  return str[0].toUpperCase() + str.slice(1);
}

alert( ucFirst("john") ); // John

在沙箱中打开带有测试的解决方案。

重要性:5

编写一个函数 checkSpam(str),如果 str 包含“viagra”或“XXX”,则返回 true,否则返回 false

该函数必须不区分大小写

checkSpam('buy ViAgRA now') == true
checkSpam('free xxxxx') == true
checkSpam("innocent rabbit") == false

打开一个带有测试的沙箱。

为了使搜索不区分大小写,让我们将字符串转为小写,然后进行搜索

function checkSpam(str) {
  let lowerStr = str.toLowerCase();

  return lowerStr.includes('viagra') || lowerStr.includes('xxx');
}

alert( checkSpam('buy ViAgRA now') );
alert( checkSpam('free xxxxx') );
alert( checkSpam("innocent rabbit") );

在沙箱中打开带有测试的解决方案。

重要性:5

创建一个函数 truncate(str, maxlength),检查 str 的长度,如果它超过 maxlength,则用省略号字符 "…" 替换 str 的末尾,使其长度等于 maxlength

函数的结果应该是截断(如果需要)的字符串。

例如

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

truncate("Hi everyone!", 20) == "Hi everyone!"

打开一个带有测试的沙箱。

最大长度必须是 maxlength,所以我们需要将它剪得短一些,为省略号留出空间。

请注意,实际上省略号只有一个 Unicode 字符。那不是三个点。

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

在沙箱中打开带有测试的解决方案。

重要性:4

我们有一个形式为 "$120" 的成本。也就是说:美元符号在前,然后是数字。

创建一个函数 extractCurrencyValue(str),从这样的字符串中提取数字值并返回它。

示例

alert( extractCurrencyValue('$120') === 120 ); // true

打开一个带有测试的沙箱。

function extractCurrencyValue(str) {
  return +str.slice(1);
}

在沙箱中打开带有测试的解决方案。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题 或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入一些代码,请使用 <code> 标记,对于多行,请将其包装在 <pre> 标记中,对于超过 10 行,请使用沙箱(plnkrjsbincodepen…)