字符串编码相关知识
字符的 Unicode 表示法
-
ES6 加强了对 Unicode 的支持,允许采用
\uxxxx
形式表示一个字符,其中xxxx
表示字符的 Unicode 码点。 -
限于码点在
\u0000
~\uFFFF
之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。 -
直接在
\u
后面跟上超过0xFFFF
的数值会理解错误。 -
改进方案,只要将码点放入大括号,就能正确解读该字符
"\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 改进后表示法
- \ddd:后面跟三位bai八进制数,该三位八进制数的值即为对应的八进制ASCII码值。
- \xhh:后面跟两位十六进制数,该两位十六进制数为对应字符的十六进制ASCII码值。
JavaScript3 种常用的进制表示方法
- 二进制0b开头
- 八进制0o开头
- 十六进制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()
的行为。如果遇到0xD800
到0xDFFF
之间的单个码点,或者不存在的配对形式,它会返回转义字符串。
例如:仅最后的语句返回 ""𝌆""
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}?`
模板字符串有哪些特点?
- 支持字符串的换行与空格缩进。
- 在模板字符串中使用反引号,则前面要用反斜杠转义。
- 支持使用
${}
嵌入变量。 ${}
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。${}
大括号内部还能调用函数。${}
大括号内部是一个字符串,将会原样输出。- 模板字符串甚至还能嵌套。
模板编译
标签模板
什么是标签模板?
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。
标签模板有哪些常见的应用?
- 过滤 HTML 字符串,防止用户输入恶意内容。
- 多语言转换(国际化处理)。
- 可以在 JavaScript 语言之中嵌入其他语言。
模板字符串的限制
-
模板字符串默认会将字符串转义(
\u
和\x
等),导致无法嵌入其他语言。 -
ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回
undefined
,而不是报错,并且从raw
属性上面可以得到原始字符串。 -
对字符串转义的放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错。
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提供的charAt
,charCodeAt
无法返回我们想要的值。
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 提供了两种方法。
-
一种是直接提供带重音符号的字符,比如
Ǒ
(\u01D1)。 -
另一种是提供合成符号(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
次。
- 参数如果是小数,会被取整。
- 如果
repeat
的参数是负数或者Infinity
,会报错。 - 如果参数是 0 到-1 之间的小数,则等同于 0,原理为规则一。
- 参数
NaN
等同于 0。 - 参数是字符串,则会先转换成数字。
'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()
一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
- 原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
- 用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。
- 省略第二个参数,默认使用空格补全长度。
'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'.padStart(10, '0') // "0000000001"
-
提示字符串格式
'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()的特征有哪些?
- 返回的都是新字符串,不会修改原始字符串。
- 除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。
trimLeft()
是trimStart()
的别名,trimRight()
是trimEnd()
的别名(为了兼容)。