第05章 字符串

284 阅读15分钟

一、概述

在JavaScript中,所谓字符串就是包含在英文“双引号”或‘单引号’中的内容,可以是数字、运算符号、各国语言、特殊编码字符,甚至还能是HTML的标签。也就是说,只要符合在双引号或单引号中这个要求,计算机文本中的内容基本上都可以被称为一个字符串,ES6新引入了模板字符串,使用反引号(`)标识。

二、字符串引号使用规则 *

在使用字符串的引号时需要注意,如果一个字符串已经使用过引号,若再在字符串内使用引号需要使用另外一个引号。即双引号内使用单引号,单引号内使用双引号,或者使用转义符 \ 对同样的引号进行转换,多层引号嵌套,该规则同样适用,如:

var str1 = "Hello, I'm Petter!";
var str2 = 'How do you think about "JavaScript"?';
var str3 = "设置字体的'颜色'代码是:<p style=\"color:red;\">";	

需要了解的一个字符串常识就是,字符串不能直接分成多行去写,否则浏览器会报错。如果字符串过长,需要进行换行使视觉层次上更加地清晰,那需要这样去写:

var str = "这是一个" + 
          "分行写的" +
          "字符串";
console.log(str); // 输出:这是一个分行写的字符串

三、字符串与数组 *

通过上一章中学习的内容可以发现 parseInt()parseFloat() 方法可以将字符串转换为数值,同时也能对数组元素中的第一个元素进行数值转化。这说明在某种程度上,字符串和数组是有一点联系的。实际上,数组拥有的很多属性,字符串也同样具备。来看这样一个输出例子:

var str = 'ABCDE';
var arr = ['A', 'B', 'C', 'D', 'E'];

console.log(str.length); // 5
console.log(arr.length); // 5

通过上例可以清晰地发现,字符串和数组是何等的相似。实际上,数组和字符串是可以互相转换的,这就需要用到两个方法:split()join()

// 1、split 将字符串转换为数组

var result;
var str = "HTML,CSS,JavaScript,jQuery";

result = str.split();
console.log(result); // ["HTML,CSS,JavaScript,jQuery"]

result = str.split(',');
console.log(result); // ["HTML", "CSS", "JavaScript", "jQuery"]

// 2、join 将数组转换为字符串

var arr = ["152", "2888", "5771"];

result = arr.join();
console.log(result); // "152,2888,5771"

result = arr.join('-');
console.log(result); // "152-2888-5771"

从上例可以分析出 split() 方法可以将一个字符串转换为一个数组,若添加一个用引号引起来的参数,得出的数组就会以该参数进行分割(上例中使用的是“,”作为参数,用其它字符作为参数同样可行),若所给参数是一个空引号"",则形成的数组会合并为一个数组元素项。同样的,join() 方法是将一个数组元素转换为一个字符串,根据给出的参数对结果中的字符串进行分割,或者不进行分割。当然,这两个方法也可以不给出参数,用它们内置的默认值进行处理。

四、转义符

反斜杠 \ 在字符串中有特殊用途,用来表示一些特殊的字符,所以又称 转义操作符(简称:转义符),以下是一些常用转义符的表示法:(小括号中的值表示法为Unicode

\n(或:\u000A)用于表示:换行符

\t(或:\u0009)用于表示:制表符

\'(或:\u0027)用于表示:单引号

\"(或:\u0022)用于表示:双引号

\\(或:\u005C)用于表示:反斜杠

当然,转义符远不止上面列出的这些。你需要注意的是,在非特殊字符前面加上转义符 \,那 \ 会被省略掉,如果需要输出 \,那就需要写成双斜杠 \ \ 的形式。

五、UNICODE

1、字符的 Unicode 表示法

JavaScript 允许采用 \uxxxx 形式表示一个字符,其中 xxxx 表示字符的 Unicode 码点,每个Unicode码都有各自对应的字符,如 \u00A9 就是用于输出版权符号“©”的。

在JavaScript引擎内部,所有字符都用Unicode表示,它不仅以Unicode储存字符,还允许直接在程序中使用Unicode编号表示字符。解析代码的时候,JavaScript会 自动识别 一个字符是字面形式表示,还是Unicode形式表示。输出给用户的时候,所有字符都会转成字面形式。其实也就是说,任何JavaScript中允许的字符都是可以通过Unicode来表示的。

"\u738b\u8005\u8363\u8000"
"王者荣耀"

我们还需要知道,每个字符在JavaScript内部都是以16位(即2个字节)的UTF-16格式储存。也就是说,JavaScript的单位字符长度固定为16位长度,即2个字节。因此,\uxxxx 这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

"\u20BB7" // "₻7"
"\uD842\uDFB7" // "𠮷" 

上面代码表示,如果直接在\u后面跟上超过0xFFFF的数值(比如\u20BB7),JavaScript会理解成 \u20BB+7。由于 \u20BB 是一个不可打印字符,所以只会显示一个空格,后面跟着一个7。ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。

"\u{20BB7}" // "𠮷"

2、str.charCodeAt(idx)   

在ES5中,如果想将一个现成的字符转换为十六进制的Unicode的话,需要通过 str.charCodeAt(idx)方法获取字符数字编码值,其中,str 为原字符串,idx为需要获取数字编码的字符对应的下标。然后再通过 toString(16) 将刚才得到的数字编码值转化为一个十六进制的字符,并在这个十六进制的字符的前面拼接上 \u,就可以得到一个十六进制表示法的Unicode了。

/**
 * 函数封装:将字符串转为Unicode编码
 */
Object.prototype.toUnicodeString = function(str) {
    var s = str || this.valueOf() ;
    var res = "";
    for (var i = 0; i < s.length; i++) {
        res += "\\u" + s.charCodeAt(i).toString(16);
    }
    return res;	
}
"王者荣耀".toUnicodeString(); // "\u738b\u8005\u8363\u8000"

但你需要注意,正如上一小节所说,JavaScript内部,字符以UTF-16的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode码点大于0xFFFF的字符),JavaScript会认为它们是两个字符。

var s = "𠮷";

s.length // 2

s.charAt(0) // "�"
s.charAt(1) // "�"

s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271

上面代码中,汉字**“𠮷”**(注意,这个字不是”吉祥“的”吉“)的码点是0x20BB7,UTF-16编码为 0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript不能正确处理,字符串长度会误判为2,而且 charAt 方法无法读取整个字符,charCodeAt 方法只能分别返回前两个字节和后两个字节的值。

3、String.fromCharCode()

在ES5中,我们可以通过String.fromCharCode(numCode)(这里的“numCode”为转换出的数字编码),又重新将数字编码转换为原来的字符串:

var str = "帅";
var numCode = str.charCodeAt(0); // 24069
var oriStr  = String.fromCharCode(numCode); // "帅"

这个方法不能识别32位的UTF-16字符(即Unicode编号大于0xFFFF 的字符)。

String.fromCharCode(0x20BB7) // // "ஷ"

提示:这一小节作为了解

六、包装对象 *

JavaScript语言“一切皆对象”,数组和函数本质上都是对象,就连三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。

JS中共有三种包装对象:即数值、字符串、布尔值相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。

var v1 = new Number(10);
var v2 = new String("Hi");
var v3 = new Boolean("true");

上面代码根据原始类型的值,生成了三个对象,与原始值的类型不同。这用typeof运算符就可以看出来。

typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"

可见:当上述三个对象充当构造函数时,可以将原始类型的值转换为对象;当充当普通函数时,可以将任意类型的值,转换为原始类型的值。

包装对象即对象,因此继承Object对象提供的原生方法。主要有一个方法叫valueOf(),这个方法的作用是返回包装对象实例对应的原始类型的值----即还原原始类型。

v1.valueOf() // 10

如何判断一个对象是内置对象还是包装对象?

在 JavaScript中对象主要分为两大类,一类称作“包装对象”,一类称作“内置对象”。从编码角度讲,它们的区别是如果使用typeof之后为“object”,那么它就是一个内置对象,否则就是包装对象。

七、字符串对象 *

1、定义

var str = "Hello, world!";
var str = 'Hello, world!';
var str = `Hello, world!`;  // ES6

var str = new String("Hi!");
var str = new Object("Hi!");

2、长度

length 属性,用于返回字符串长度。

var str = "China!";
str.length; // 6

3、查询

3.1、charAt()

charAt 方法返回指定位置的字符,参数是从0开始编号的下标。其语法形式为:charAt(idx)

var s = new Object("ABCDE");
String {"ABCDE"}
0: "A"
1: "B"
2: "C"
3: "D"
4: "E"
s.charAt(2);
"C"
s.charAt(); // 如果没有传递任何参数,默认下标为0
"A"
s.charAt(s.length - 1); // 获取最后一个字符
"E"
// 如果传入的下标超出了有效下标的范围,则返回空字符串
s.charAt(5); 
""
s.charAt(-1); 
""

3.2、indexOf() 、 lastIndexOf()

这两个方法用于确定一个字符串在另一个字符串中的位置,返回一个整数,表示匹配开始的位置。如果返回-1,就表示不匹配。两者的区别在于,indexOf 从字符串头部开始匹配,lastIndexOf 从尾部开始匹配。

var s = "干包谷林头绑干包谷";

s.indexOf("包谷"); // 1
s.indexOf("玉米"); // -1
s.lastIndexOf("包谷"); //7
s.lastIndexOf("玉米"); // -1

它们还可以接受第二个参数,对于 indexOf 方法,第二个参数表示从该位置开始向后匹配;对于lastIndexOf,第二个参数表示从该位置起向前匹配。

var str = 'Hello, world!';

str.indexOf('l', 5); // 10
str.lastIndexOf('l', 5) // 3

3.3、match()

match 方法用于字符串查询,如果没有找到,则返回null,如果找到,返回一个数组。

var str = "abc";

str.match("ab"); // ["ab"]
str.match("ac"); // null

3.4、search()

search 方法的用法等同于 indexOf/lastIndexOf,但是其返回值为匹配到的第一个位置。如果没有找到匹配,则返回 -1

var str = 'Hello, china!';

str.search('china'); // 7
str.search('world'); // -1

3.5、查询头部/尾部

字符串提供了三种方法便于我们查询是头部、尾部和是否包含,具体如下:

  • startsWith:查询头部
  • endsWith:查询尾部
  • includes:查询是否包含

当然,你也可以使用正则表达式实现同样的效果:

var website = "http://www.baidu.com";
// 查询头部,正则表达式规则:/^condition/
/^http/.test(website);

// 查询尾部,正则表达式规则:/condition$/
/com$/.test(website);

// 查询是否包含,正则表达式规则:/condition/
/baidu/.test(website);

4、拼接

拼接字符串,使用+号:

var str1 = 'Hello, ';
var str2 = 'China!';
var str3 = str1 + str2; // "Hello, China!";

除了使用 + 拼接外,你还可以使用 concat 方法对字符串进行拼接,如:

var s1 = 'a';
var s2 = 'b';
s1.concat(s2);
'ab'

提示:从性能上考虑,强烈建议使用赋值操作符+, +=)代替 concat 方法。

5、截取

slice 方法用于字符串截取,其语法形式为:

slice(start, end)
  • start:开始位置
  • end:结束位置(不含该位置)
'JavaScript'.slice(0, 4) // "Java"

如果省略第二个参数,则表示从指定位置开始截取到末尾。

'JavaScript'.slice(4) // "Script"

如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度。

'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(0, -6) // "Java"
'JavaScript'.slice(-2, -1) // "p"

如果第一个参数大于第二个参数,slice 方法返回一个空字符串。

'JavaScript'.slice(2, 1) // ""

6、去除空格

trim 方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串。

'  Hello world!  '.trim() // "Hello world!"

7、大小写转换

  • toLowerCase:转小写
  • toUpperCase:转大写
"ChInA".toLowerCase();
"china"
"ChInA".toUpperCase();
"CHINA"

这个方法也可以将布尔值或数组转为大写字符串,但是需要通过call方法使用。

String.prototype.toUpperCase.call(true)
// 'TRUE'
String.prototype.toUpperCase.call(['a', 'b', 'c'])
// 'A,B,C'

8、字符串比较

JavaScript允许使用 ><== 比较字符串,返回一个布尔。其在比较时是通过字符的 Unicode 编码进行比较的。

var compare = (s1, s2) => {
	if (s1 > s2) {
		console.log(`${s1} > ${s2}`);
	}else if(s1 == s2) {
		console.log(`${s1} = ${s2}`);
	}else {
		console.log(`${s1} < ${s2}`);
	}
}

compare('a', 'b');   // "a < b"
compare('a', 'A');   // "a > A"
compare('ab', 'ac'); // "ab < ac"
compare('ab', 'ab'); // "ab = ab"

有一种情况需要注意,就是计算机的区域设置。因为用"<"和">"来比较字符串时,并不会考虑当地的排序规则,比如在西班牙语中,按照传统的排序,"ch"将作为一个字符排在"c"和"d"之间。localeCompare() 提供了一种方式,可以帮助你使用默认区域设置下的字符排序规则。

localeCompare 用本地特定的顺序来比较两个字符串。其语法形式为: stringObject.localeCompare(target),如果 stringObject 小于 target,则 localeCompare() 返回小于 0 的数。如果 stringObject 大于 target,则该方法返回大于 0 的数。如果两个字符串相等,或根据本地排序规则没有区别,该方法返回 0。

'apple'.localeCompare('banana') // -1

'apple'.localeCompare('apple')  // 0

提示:ECMAscript 标准并没有规定如何进行本地特定的比较操作,它只规定该函数采用底层操作系统提供的排序规则。

9、字符串替换

replace 方法用于字符串替换,其语法形式为:

replace(search, replacement)
  • search:替换源
  • replacement:要替换成什么数据
var str = "Hello, world!";
str.replace("world", "china"); // "Hello, china!"

replace只会替换第一次匹配到的字段,不能完全替换:

var str = "#fffff#";
str.replace("f", "o"); // "#offff#"

上述例子中只会替换第一个f,如果想要全局匹配替换,我们可以使用正则表达式,如下所示:

// g -> global:全局模式
str.replace(/f/g, "o"); // "#ooooo#"

replace 方法的第2个参数也可以是一个函数,如下所示:

"Tel:17398888669;".replace(/(\d{3})(\d{4})(\d{4})/, (match,$1, $2, $3, offset, string) => {
    console.log(match, $1, $2, $3, offset, string);
    return `${$1} ${$2} ${$3}`;
})
// 17398888669 173 9888 8669 4 Tel:17398888669;
// Tel:173 9888 8669;

可以看到,当参数为函数时,其中各参数表示:

  • match:匹配的字符串(17398888669)。
  • $1:如果 replace 放法的第1个参数是正则表达式,则 $1 表示第1个括号(组匹配)匹配的字符串,以此类推,$2 / $3 表示第2 / 3 个括号匹配字符串。
  • offset:偏移量,表示被匹配到的字符串在原始字符串中的位置。
  • string:被匹配的原始字符串。

替换字符串可以插入下面的特殊变量名:

变量名代表的值
$$插入一个 "$"。
$&插入匹配的子串。
$`插入当前匹配的子串左边的内容。
$'插入当前匹配的子串右边的内容。
$n假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。提示:索引是从1开始。如果不存在第 n个分组,那么将会把匹配到到内容替换为字面量。比如不存在第3个分组,就会用“$3”替换匹配到的内容。
$<Name>这里*Name* 是一个分组名称。如果在正则表达式中并不存在分组(或者没有匹配),这个变量将被处理为空字符串。只有在支持命名分组捕获的浏览器中才能使用。

我们简单应用下:

// 1. 加密手机
"17398888669".replace(/(\d{3})(\d{4})(\d{4})/, "$1 *** $3")
// -> '173 *** 8669'

// 2. 交换两个单词
var re = /(\w+)\s(\w+)/;
var str = "John Smith";
var newstr = str.replace(re, "$2, $1");
// Smith, John
console.log(newstr);

八、编码

1. Base64转码

Base64是一种编码方法,可以将任意字符转成可打印字符。这种编码方法,一开始的作用是为了不显示特殊字符,简化程序的处理。但因为这种编码方式比较复杂,现在使用JavaScript的开发者都少有涉及这种编码方式,所以更多时候都将其作为一种加密手段在使用。该编码提供两个方法来转换字符:

  • btoa()

    将字符串或二进制值转为Base64编码

  • atob()

    将Base64编码转为原来的编码

我们先来看一个将普通字符串转换为Base64格式编码,再将该格式转换为普通字符串在控制台中输出的例子:

part_4_13.jpeg

可以从示例中的前两个结果很直观的发现,我们首先是用btoa()将一串普通的字符串“Hello, world!”转换成为了Base64码“YXVsZW5jZQ==”,然后我们将得出的Base64码通过atob()方法将得到的Base64码再次成功地转换成为了普通的字符串“Hello, world!”。示例中的后两个结果我们是对二进制数“0b1001”进行了相同方式的转换,最后得到字符串“9”(若用parseInt(0b1001),得出的结果为数字9,而不是字符串“9”)。可见这两种方法是可以将字符串和Base64码在两种格式之间互相转换的,但是要注意的是,通过atob()这种方法是不能还原出数值型的值的,这为我们拼接密码字符串提供了便利(因为“+”运算符只能做拼接运算了)。但在模型情况下需要得出的结果是一个数值型的值,那就得用“数值的转换”提供的三种方法来进行转换了。

但是在使用这两个方法的时候需要注意的是,这两个方法不支持对非ASC II字符转换为Base64码,否则在浏览器中会报错。如果要对非ASC II的字符进行Base64转码,需要用到两个对URI组件编码的函数:“encodeURIComponent()”函数和“decodeURIComponent()”函数,前者能将字符串作为 URI 组件进行编码,后者能将encodeURIComponent() 函数编码的 URI 进行解码。如下所示:

part_4_14.jpeg

2. encodeURI && decodeURI

  • encodeURI:编码,不可以编码特殊字符(#/& 等)
  • decodeURI:解码,不可以解码特殊字符(#/& 等)

3. encodeURIComponent && decodeURIComponent

  • encodeURIComponent:编码,可以编码特殊字符(#/& 等)

  • decodeURIComponent:解码,可以解码特殊字符(#/& 等)

九、课后练习

1. 将字符串 “hello” 逆序输出为 “olleh”

2. 删除字符串 “01i13P47h2o39n32e09” 中的所有数字,输出结果

3. 定义一个字符串 ”CHINESE“,将其输出为 “Chinese“ 

4. 统计 ”warriors” 单词中,“r“字母出现的次数

5. 将字符串 ”-_-” 中的 “_“ 替换成 ”$”

6. 将字符串”border-bottom-color” 转换成驼峰命名”borderBottomColor”

7. 输入身份证号,点击按钮,显示出生年月,输出格式为:"出生年月:xxxx年xx月xx日"
  > 提示:
  - 获取元素:document.querySelector(CSS选择器);
  - 点击事件:el.onclick = function(){};
  - 获取输入框的值:el.value;
  - 设置元素显示内容:el.textContent = "";