JS全书:JavaScript Web前端开发指南
上QQ阅读APP看书,第一时间看更新

3.4 字符串

字符串可以由单引号(')或双引号(")或反撇号(`)直接包裹0个或多个字符组成,像这样的方式直接标识一个字符串也称为字符串字面量表示法,除此之外,还可以通过构造函数String来创建一个字符串。

在上一节中,我们已经知道,这两种方式创建的字符串除了类型不同,其值相等,且拥有相同的属性和方法。在日常使用中区别不大,一般情况下,建议使用字符串的字面量表示法,这样更方便,示例如下。

      let foo = 'string';
      let bar = "string";
      let baz = `string`;
      let qux = new String('string');

但需要注意的是,以何种引号/反撇号开头就必须以相同的引号/反撇号结尾,像下面这样的使用方式将会报错。

      //以下是错误的使用方式
      let foo = 'string";
      let bar = 'string`;
      let baz = `string";
      let qux = new String('string');

如果字符串中含有具有特殊功能的字符串,就需要使用转义字符进行转义。转义字符由一个反斜杠和一个或多个字符组成,表3-2列举了一些常见的转义字符。

表3-2 转义字符

例如,下面的代码就会造成歧义与报错。

      let foo = 'stri'ng'; // ->报错

这时就需要用到转义字符:

      let foo = 'stri\'ng';

在字符串字面量表示法中,单引号和双引号的使用方法相同,反撇号为ES6中新引入的一种字符串字面量语法,可以用它来代替单引号和双引号。在JavaScript中,反撇号又被称为“模板字符串”(template strings)。

在模板字符串中,可以包含特定的表达式占位符,JavaScript会解析这些表达式占位符并返回解析后的字符串,示例如下。

      let foo = 'hello';

let bar = `${foo} world`;
console.log(bar); // > "hello world"

这里的表达式不仅只能是一个变量,也可以是其他JavaScript语句,示例如下。

      //可以进行运算
      `${1+1} = ${2}`; // -> "2 = 2"

//可以是一个函数 `${foo()}`;
...

为什么需要模板字符串呢?随着Web的应用化,JavaScript需要做的事也越来越多,拼接字符串更是在代码中随处可见,示例如下。

普通字符串无法表示多行字符串,因此需要使用加号连接符进行连接,上面的写法使用了大量的加号连接字符串,这就导致代码阅读起来并不是很直观,尤其是在处理大量复杂数据的情况下,这种写法很容易导致代码出现问题(写代码时基本是没有问题的,但当接手别人的项目或维护自己很久之前写的代码时,这种写法就显得比较乱,对这类代码进行修改时也容易出现问题)。

模板字符串正是在这样的环境下产生的,使用模板字符串可以很好地解决上面的问题,示例如下。

这样,我们的代码看起来就很清晰了,但要注意的是,模板字符串会保留其中的空格、换行符、制表符等。因此,在上面的代码中,就多出了一些空格和换行符,我们可以使用trim()方法来去除这些多余的字符。

3.4.1 字符串的特点

字符串是不可变的,即一旦字符串被创建,该字符串的值就不能改变了,如果要改变一个变量中包含的字符串的值,需要用另一个字符串赋值给该变量,原来的字符串将会被销毁。

这个过程与上一节中的基本包装类型类似,示例如下。

      let foo = "hello";

foo = "world";
console.log(foo); // > "world";

上述代码可以看作是执行了类似下面的操作。

      let foo = "hello";

foo = new String("world"); // 销毁 "hello"、 销毁 "world"
console.log(foo); // > "world"

上述代码中,对变量foo赋值时,实际上是创建了一个新的字符串,并将该字符串的值赋值给变量foo,并不是直接将"hello"的值修改为"world",而在变量foo的值发生改变后,"hello"不再被使用,将会被直接销毁。

为了进一步验证上面的结果,我们直接修改原字符串的值。

同样,下面的示例中,"hello world"是以"hello"和" world"创建的两个新字符串的值连接后返回的结果。

      let foo = "hello" + " world";

// foo = new String("world") + new String(" world");
console.log(foo); // > "hello world"

3.4.2 length属性

length是一个属性,而不是一个方法,length属性返回字符串的长度,示例如下。

      'string'.length; // -> 5

如果字符串中包含转义字符,每个转义字符会被解析成一个字符,示例如下。

3.4.3 实例方法

JavaScript提供了一些方法用来对字符串进行操作,这些方法看起来很多,但实际上很多方法都是类似的。

这些方法不会修改字符串本身,而是返回一个新的字符串或其他值。

1. str.charAt([index])和str.charCodeAt([index])

str.charAt([index])和str.charCodeAt([index])这两个方法用来对字符串中指定索引处的字符进行取值,并将所取得的值返回。其中,charAt()方法取的是字符串中指定索引index处的字符,而charCodeAt()方法取的是该字符的Unicode码点。

索引index的取值区间为[0,length-1]的数字(length为字符串的长度),如果索引不是数字,将会隐式转换成一个数字,默认为0,示例如下。

在JavaScript中,索引是从0开始的,因此取索引为0处的字符,取得就是第一个字符。

如果索引的值不是整数,这两个方法都会将该值进行去尾操作,并使用去尾后的数值作为索引,示例代码如下。

      let foo = "hello world";

// 非整数索引取字符 foo.charAt(0.9); // -> "h"
// 非整数索引取字符的 `Unicode` 码点 foo.charCodeAt(0.9); // -> 104

对于索引不在区间内的,charAt()方法将会返回一个空字符串,而charCodeAt()则是返回NaN,示例如下。

      let foo = "hello world";

// 取字符 foo.charAt(-1); // -> ""
// 取字符的 `Unicode` 码点 foo.charCodeAt(-1); // -> NaN

不过,一般情况下很少使用charAt()方法,因为还有另外一种更便捷的方式可以获取指定索引出的字符,示例如下。

      foo[0];  // -> "h"

// 这种方式不会对索引值进行类型转换,也不会执行去尾操作 foo[0.9]; // -> undefined foo[null]; // -> undefined foo[-1]; // -> undefined
2. str.codePointAt(index)

在JavaScript内部,字符是以UTF-16的格式存储的,每个字符占用2个字节,但有些字符可能会占用4个字节(两个UTF-16,又称UTF-32),charCodeAt()方法不能正确处理这些需要占用4个字节的字符,codePointAt()方法正是为了解决这个问题而出现的。

这个方法与charCodeAt()方法类似,同样是返回指定索引处字符的码点,但不同之处在于,codePointAt()方法的返回值如下。

  • 如果该索引处没有字符,则返回undefined。
  • 如果该字符占用4个字节,则返回该字符的UTF-32的码点。
  • 如果该字符占用2个字节,则返回该字符的UTF-16的码点。

示例如下。

3. str.concat(str1[,…, strN])

concat()方法把一个或多个字符串与str连接,并返回一个新的字符串,示例如下。

      "hello".concat(" world"); // -> "hello world"

不过,我们一般用+运算符来代替这个方法,示例如下。

      "hello" + " world"; // -> "hello world"
4. str.indexOf(searchString[, fromIndex])和str.lastIndexOf(searchString[, fromIndex])

str.indexOf(searchString[, fromIndex])和str.lastIndexOf(searchString[, fromIndex])这两个方法用来查询给定的字符串searchString在str中首次出现的位置,如果没有找到,就返回-1,fromIndex为可选参数,表示从何处开始(包含这个位置)查询。

在indexOf()中,fromIndex默认为0,如果fromIndex小于0,也会被当作0处理,示例如下。

上述代码中,不指定fromIndex,或指定fromIndex小于等于0,表示的是从字符串的起始位置开始往后查询,遇到的第一个"o"的索引为4,因此返回4;当指定fromIndex为5时,将从字符串的索引位置为5的位置(空字符)开始往后查询,直到遇到第一个"o"的索引为7,返回索引7。

当fromIndex的值超出字符串的最大索引,即fromIndex的值大于等于字符串的长度时,将会返回-1。通过前文我们已经知道,对于索引不在区间内的,charAt()方法将会返回一个空字符串,也就是说,可以看作是超出字符串长度的是全都是空字符串,如果要查找的字符串不为空,那么,在超出最大索引的位置外就找不到匹配的字符,因此返回-1。

示例代码如下。

lastIndexOf()方法与indexOf()方法类似,只不过查询方式与indexOf()相反,是从后向前查找的。

在lastIndexOf()中,fromIndex默认为字符串的最大索引值(以下简称str.length-1),如果fromIndex大于最大索引值,也会被当作最大索引值处理,示例如下。

当fromIndex的值小于等于0时,示例如下。

可以用这两个方法判断一个字符串是否包含另一个字符串,示例如下。

      let foo = "hello world";

if(foo.indexOf('hello') !== -1){ // ... }
5. str.includes(searchString[, fromIndex])、str.startsWith(searchString [, fromIndex])和str.ends With(searchString [, fromIndex])

ES6中新增了3种方法来判断一个字符串是否被另一个字符串包含(此前,只能通过对lastIndexOf()与indexOf()返回的索引值进行判断来判断),如果被包含,则返回true,否则,返回false。

fromIndex为可选参数,表示从何处开始查询,默认为0,示例如下。

endsWith()方法与lastIndexOf()类似,也是从后向前查找的,但endsWith()是从fromIndex()之后(不包含当前指定的索引位置)开始查询的,示例如下。

6. str.search(regexp)

str.search(regexp)方法与indexOf()相同,返回匹配到的字符串在str中首次出现的位置,如果没有找到,则返回-1,只不过search()方法接收一个正则表达式作为参数(非正则表达式会被转换为正则表达式处理),示例如下。

      "hello world".search(/l/); // -> 2
      "hello world".search(/a/); // -> -1

// 非正则表达式转为正则表达式处理 "hello world".search("l"); // -> 2 "hello world".search("a"); // -> -1
7. str.localeCompare(compareStr)

localeCompare()方法用来比较两个字符串,如果str小于compareStr,则返回负值,如果str等于compareStr,则返回0,如果str大于compareStr,则返回正值,示例如下。

      // Chrome 59
      "a".localeCompare("b");  // -> -1
      "a".localeCompare("a");  // -> 0
      "b".localeCompare("a");  // -> -1
8. str.padStart(targetLength [, char])和str.padEnd(targetLength [, char])

ES8中引入了padStart()和padEnd()两个方法用来补全字符串,targetLength为补全后字符串的长度,这两个方法的返回值都是补全后的字符串。

char为可选参数,表示用来补全字符串的字符,默认为空格,示例如下。

如果targetLength小于当前字符串的长度,则返回当前字符串,示例如下。

9. str.repeat([count])

repeat()方法将重复count次后的str连接后返回。

count为可选参数,表示重复的次数,取值区间为[0, Infinity),默认为0,传入区间外的值,会抛出错误RangeError,示例如下。

10. str.replace(oldValue, newValue)

replace()方法将str中的oldValue替换为newValue。

oldValue可以是一个字符串或正则表达式,具体如下。

  • oldValue为字符串时,将匹配到的第一个oldValue替换为newValue。
  • oldValue为正则表达式时,将匹配到的oldValue替换为newValue。

示例如下。

newValue表示用来替换oldValue的新值,newValue可以是一个字符串或函数,具体如下。

  • newValue为字符串时,该字符串内可插入一些具有特殊含义的字符。
  • newValue为函数时,函数的返回值将被作为替换字符串处理。

示例如下。

此外,newValue中还有一些特殊的字符表示匹配的结果,如表3-3所示。

表3-3 特殊字符

示例如下。

11. str.slice(startIndex[, endIndex])和str.substring(startIndex[, endIndex])

slice()方法从str中截取一段字符串,并将其返回。startIndex表示从何处开始截取,默认为0;endIndex为可选参数,表示截取到何处(不包含endIndex),默认为字符串的长度,示例如下。

substring()方法与slice()方法类似,示例如下。

不同之处在于,slice()的参数为负值时,会被当作参数index+字符串长度length处理,示例如下。

substring()则将负值当作0处理,示例如下。

      "hello world".substring(-1); // -> "hello world"

如果不知道某个字符串的索引,那么此时就需要用到前面所学的indexOf()和lastIndexOf(),或者search()方法。

12. str.substr(startIndex[, subLength])

substr()方法与slice类似,也是从str中截取一段字符串,并将其返回。fromIndex可以为负值,只不过substr()的subLength表示的是截取的字符串的字符个数,默认截取到字符串的末尾,示例如下。

13. str.split([separator[, limit]])

split()方法将字符串用分隔符separator分割成数组,并将其返回。limit为可选参数,表示返回的数组长度,limit为负值时会被忽略。

分隔符separator支持正则表达式,示例如下:

14. str.toLocaleLowerCase()、str.toLocaleUpperCase()、str.toLowerCase()和str.toUpperCase()

这四个方法可以将字符串中的字母(以下简称“特定字符”)进行大小写转化,并返回转化后的字符串,具体如下。

  • toLocaleLowerCase使用本地化的规则将特定字符转化为小写。
  • toLocaleUpperCase使用本地化的规则将特定字符转化为大写。
  • toLowerCase将特定字符转化为小写。
  • toUpperCase将特定字符转化为大写。

示例如下。

15. str.trim()、str.trimLeft()和str.trimRight()

这三个方法去除字符串左右两边的空格,并返回新的字符串,具体如下。

  • trim:去除字符串两边的空格。
  • trimLeft:去除字符串左边的空格。
  • trimRight:去除字符串右边的空格。

示例如下。

练习

  • 创建一个字符串,在其上调用不同的字符串方法,并对比调用方法后返回值与原字符串的区别。