ECMAScript 字符串的扩展
介绍 ES6 对字符串的改造和增强
本章介绍 ES6 对字符串的改造和增强,下一章介绍字符串对象的新增方法。
ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。
上面代码中,最后一个例子表明,大括号表示法与四字节的 UTF-16 编码是等价的。
有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。
ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for...of
循环遍历。
除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF
的码点,传统的for
循环无法识别这样的码点。
上面代码中,字符串text
只有一个字符,但是for
循环会认为它包含两个字符(都不可打印),而for...of
循环会正确识别出这一个字符。
JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。举例来说,“中”的 Unicode 码点是 U+4e2d,你可以直接在字符串里面输入这个汉字,也可以输入它的转义形式\u4e2d
,两者是等价的。
但是,JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。
- U+005C:反斜杠(reverse solidus)
- U+000D:回车(carriage return)
- U+2028:行分隔符(line separator)
- U+2029:段分隔符(paragraph separator)
- U+000A:换行符(line feed)
举例来说,字符串里面不能直接包含反斜杠,一定要转义写成\\
或者\u005c
。
这个规定本身没有问题,麻烦在于 JSON 格式允许字符串里面直接使用 U+2028(行分隔符)和 U+2029(段分隔符)。这样一来,服务器输出的 JSON 被JSON.parse
解析,就有可能直接报错。
JSON 格式已经冻结(RFC 7159),没法修改了。为了消除这个报错,ES2019 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。
根据这个提案,上面的代码不会报错。
注意,模板字符串现在就允许直接输入这两个字符。另外,正则表达式依然不允许直接输入这两个字符,这是没有问题的,因为 JSON 本来就不允许直接包含正则表达式。
根据标准,JSON 数据必须是 UTF-8 编码。但是,现在的JSON.stringify()
方法有可能返回不符合 UTF-8 标准的字符串。
传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
上面代码中,所有模板字符串的空格和换行,都是被保留的,比如<ul>
标签前面会有一个换行。如果你不想要这个换行,可以使用trim
方法消除它。
模板字符串中嵌入变量,需要将变量名写在${}
之中。
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
模板字符串之中还能调用函数。
如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString
方法。
如果模板字符串中的变量没有声明,将报错。
由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。
模板字符串甚至还能嵌套。
上面代码中,模板字符串的变量之中,又嵌入了另一个模板字符串,使用方法如下。
如果需要引用模板字符串本身,在需要时执行,可以写成函数。
上面代码中,模板字符串写成了一个函数的返回值。执行这个函数,就相当于执行这个模板字符串了。
下面,我们来看一个通过模板字符串,生成正式模板的实例。
上面代码在模板字符串之中,放置了一个常规模板。该模板使用<%...%>
放置 JavaScript 代码,使用<%= ... %>
输出 JavaScript 表达式。
怎么编译这个模板字符串呢?
一种思路是将其转换为 JavaScript 表达式字符串。
这个转换使用正则表达式就行了。
然后,将template
封装在一个函数里面返回,就可以了。
将上面的内容拼装成一个模板编译函数compile
。
compile
函数的用法如下。
模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。
标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。
但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
上面代码中,模板字符串前面有一个标识名tag
,它是一个函数。整个表达式的返回值,就是tag
函数处理模板字符串后的返回值。
函数tag
依次会接收到多个参数。
tag
函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。
tag
函数的其他参数,都是模板字符串各个变量被替换后的值。由于本例中,模板字符串含有两个变量,因此tag
会接受到value1
和value2
两个参数。
tag
函数所有参数的实际值如下。
- 第一个参数:
['Hello ', ' world ', '']
- 第二个参数: 15
- 第三个参数:50
也就是说,tag
函数实际上以下面的形式调用。
我们可以按照需要编写tag
函数的代码。下面是tag
函数的一种写法,以及运行结果。
下面是一个更复杂的例子。
上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。
passthru
函数采用 rest 参数的写法如下。
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
上面代码中,sender
变量往往是用户提供的,经过SaferHTML
函数处理,里面的特殊字符都会被转义。
标签模板的另一个应用,就是多语言转换(国际化处理)。
模板字符串本身并不能取代 Mustache 之类的模板库,因为没有条件判断和循环处理功能,但是通过标签函数,你可以自己添加这些功能。
除此之外,你甚至可以使用标签模板,在 JavaScript 语言之中嵌入其他语言。
上面的代码通过jsx
函数,将一个 DOM 字符串转为 React 对象。你可以在 GitHub 找到jsx
函数的具体实现。
下面则是一个假想的例子,通过java
函数,在 JavaScript 代码之中运行 Java 代码。
模板处理函数的第一个参数(模板字符串数组),还有一个raw
属性。
上面代码中,console.log
接受的参数,实际上是一个数组。该数组有一个raw
属性,保存的是转义后的原字符串。
请看下面的例子。
上面代码中,tag
函数的第一个参数strings
,有一个raw
属性,也指向一个数组。该数组的成员与strings
数组完全一致。比如,strings
数组是["First line\nSecond line"]
,那么strings.raw
数组就是["First line\\nSecond line"]
。两者唯一的区别,就是字符串里面的斜杠都被转义了。比如,strings.raw 数组会将\n
视为\\
和n
两个字符,而不是换行符。这是为了方便取得转义之前的原始模板而设计的。
前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言。
举例来说,标签模板里面可以嵌入 LaTEX 语言。
上面代码中,变量document
内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的,但是 JavaScript 引擎会报错。原因就在于字符串的转义。
为了解决这个问题,ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined
,而不是报错,并且从raw
属性上面可以得到原始字符串。
上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义的限制,所以不报错了,JavaScript 引擎将第一个字符设置为undefined
,但是raw
属性依然可以得到原始字符串,因此tag
函数还是可以对原字符串进行处理。
注意,这种对字符串转义的放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错。