跟着阮神学ES6——字符串的扩展

445 阅读11分钟

字符串编码相关知识

字符的 Unicode 表示法

  1. ES6 加强了对 Unicode 的支持,允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。

  2. 限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

  3. 直接在\u后面跟上超过0xFFFF的数值会理解错误。

  4. 改进方案,只要将码点放入大括号,就能正确解读该字符

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

JavaScript中6 种表示一个字符的方法

'z' === 'z'  // true
'\z' === 'z'  // true   122
'\172' === 'z' // true  1*8*8 + 7*8 +2 = 122  // 对应下面的\ddd模式
'\x7A' === 'z' // true  7*16 + 10             // 对应下面的\xhh模式
'\u007A' === 'z' // true				    // Unicode 表示法
'\u{7A}' === 'z' // true				    // Unicode 改进后表示法
  1. \ddd:后面跟三位bai八进制数,该三位八进制数的值即为对应的八进制ASCII码值。
  2. \xhh:后面跟两位十六进制数,该两位十六进制数为对应字符的十六进制ASCII码值。

JavaScript3 种常用的进制表示方法

  1. 二进制0b开头
  2. 八进制0o开头
  3. 十六进制0x开头

字符串的遍历器接口

传统的for循环无法识别大于0xFFFF的码点,而for...of可以。

for (let i of text) {
  console.log(i);
}
// "𠮷" 

字符串转义

JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。可以直接在字符串里面输入这个汉字,也可以输入它的转义形式\u4e2d,两者是等价的。

'中' === '\u4e2d' // true

JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。

  • U+005C:反斜杠(reverse solidus)

  • U+000D:回车(carriage return)

  • U+2028:行分隔符(line separator)

  • U+2029:段分隔符(paragraph separator)

  • U+000A:换行符(line feed)

为消除JSON JSON.parse解析行分隔符与段分隔符的异常,ES2019 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。

注意,模板字符串现在就允许直接输入这两个字符。另外,正则表达式依然不允许直接输入这两个字符,这是没有问题的,因为 JSON 本来就不允许直接包含正则表达式。

JSON.stringify() 调整

JSON 数据必须是 UTF-8 编码。但是,现在的JSON.stringify()方法有可能返回不符合 UTF-8 标准的字符串。

为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD8000xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串。

例如:仅最后的语句返回 ""𝌆""

JSON.stringify('\u{D834}') // ""\\uD834""
JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""
JSON.stringify('\uD834\uDF06') 

模板字符串

什么是模板字符串?

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

模板字符串有哪些特点?

  1. 支持字符串的换行与空格缩进。
  2. 在模板字符串中使用反引号,则前面要用反斜杠转义。
  3. 支持使用${}嵌入变量。
  4. ${}大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
  5. ${}大括号内部还能调用函数。
  6. ${}大括号内部是一个字符串,将会原样输出。
  7. 模板字符串甚至还能嵌套。

模板编译

标签模板

什么是标签模板?

模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

标签模板有哪些常见的应用?

  1. 过滤 HTML 字符串,防止用户输入恶意内容。
  2. 多语言转换(国际化处理)。
  3. 可以在 JavaScript 语言之中嵌入其他语言。

模板字符串的限制

  1. 模板字符串默认会将字符串转义(\u\x等),导致无法嵌入其他语言。

  2. ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。

  3. 对字符串转义的放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错。

    let bad = `bad escape sequence: \unicode`; // 报错
    

String.fromCodePoint()

为什么需要新增String.fromCodePoint()呢?

ES5 提供String.fromCharCode()方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于0xFFFF的字符。ES6 提供了String.fromCodePoint()方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode()方法的不足。

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

String.fromCodePoint(0x20BB7)
// "𠮷"
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true

String.fromCodePoint()方法与下面的codePointAt()相反。

实例方法:codePointAt()

为什么需要codePointAt()?

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

根据以上示例发现,对于4 个字节储存的字符,ES5提供的charAtcharCodeAt无法返回我们想要的值。

ES6 提供了codePointAt()方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。

let s = '𠮷a';

s.codePointAt(0) // 134071
s.codePointAt(1) // 57271

s.codePointAt(2) // 97

codePointAt()的缺陷?

但是,codePointAt()方法的参数,仍然是不正确的。上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt()方法传入 2。

怎么规避codePointAt()的缺陷呢?

方法一:使用for...of循环

let s = '𠮷a';
for (let ch of s) {
  console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61

方法二:使用扩展运算符(...)进行展开运算

let arr = [...'𠮷a']; // arr.length === 2
arr.forEach(
  ch => console.log(ch.codePointAt(0).toString(16))
);
// 20bb7
// 61

怎么判断是否为 4 个字节储存的字符?

function is32Bit(c) {
  return c.codePointAt(0) > 0xFFFF;
}

is32Bit("𠮷") // true
is32Bit("a") // false

String.raw()

String.raw()的用途?

String.raw()方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。

String.raw`Hi\n${2+3}!`
// 实际返回 "Hi\\n5!",显示的是转义后的结果 "Hi\n5!"

String.raw()的函数写法

函数的形式,它的第一个参数,应该是一个具有raw属性的对象,且raw属性的值应该是一个数组,对应模板字符串解析后的值。

`foo${1 + 2}bar`
// 等同于
String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar"

手写String.raw()函数

String.raw = function (strings, ...values) {
  let output = '';
  let index;
  for (index = 0; index < values.length; index++) {
    output += strings.raw[index] + values[index];
  }

  output += strings.raw[index]
  return output;
}

实例方法:normalize()

normalize()方法的使用场景是什么?

许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。

  1. 一种是直接提供带重音符号的字符,比如Ǒ(\u01D1)。

  2. 另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。

这两种表示方法,在视觉和语义上都等价,但是 JavaScript 不能识别。如下,长度不一致而且相等判断为false

'\u01D1'==='\u004F\u030C' //false

'\u01D1'.length // 1
'\u004F\u030C'.length // 2

normalize()方法如何使用?

ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true

normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下:

  • NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。

  • NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。

  • NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)

  • NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。

    '\u004F\u030C'.normalize('NFC').length // 1
    '\u004F\u030C'.normalize('NFD').length // 2
    

normalize()方法的限制?

normalize方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。

实例方法:includes(), startsWith(), endsWith()

ES5 时代,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。

ES6 提供了三种新方法:

  • includes():返回布尔值,表示是否找到了参数字符串。

  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。

  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

支持第二个参数,用于限定搜索的位置

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true  针对前n个字符,即'Hello'
s.includes('Hello', 6) // false

上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。

endsWith针对前n个字符,而includes(), startsWith()方法针对从第n个位置直到字符串结束。

实例方法:repeat()

repeat方法返回一个新字符串,表示将原字符串重复n次。

  1. 参数如果是小数,会被取整。
  2. 如果repeat的参数是负数或者Infinity,会报错。
  3. 如果参数是 0 到-1 之间的小数,则等同于 0,原理为规则一。
  4. 参数NaN等同于 0。
  5. 参数是字符串,则会先转换成数字。
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
'na'.repeat(2.9) // "nana"  --参数会被取整
'na'.repeat(Infinity) // RangeError --报错
'na'.repeat(-1) // RangeError --报错
'na'.repeat(-0.9) // ""
'na'.repeat(NaN) // ""
'na'.repeat('3') // "nanana" --先转换成数字

实例方法:padStart(),padEnd()

padStart(),padEnd()怎么使用?

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。padStart()padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。

  1. 原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
  2. 用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。
  3. 省略第二个参数,默认使用空格补全长度。
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'

'xxx'.padStart(2, 'ab') // 'xxx' --补全不生效,返回原字符串
'xxx'.padEnd(2, 'ab') // 'xxx'  --补全不生效,返回原字符串

'abc'.padStart(10, '0123456789')  // '0123456abc' --会截去超出位数的补全字符串
'x'.padStart(4)  // '   x'  --默认使用空格补全长度

padStart(),padEnd()用途

  1. 数值补全指定位数

    '1'.padStart(10, '0') // "0000000001"
    
  2. 提示字符串格式

    '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
    '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
    

实例方法:trimStart(),trimEnd()

ES2019 对字符串实例新增了trimStart()trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。

trimStart(),trimEnd()的特征有哪些?

  1. 返回的都是新字符串,不会修改原始字符串。
  2. 除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。
  3. trimLeft()trimStart()的别名,trimRight()trimEnd()的别名(为了兼容)。