首先推荐两篇文章了解字符编码相关细节
Javascript中的string类型使用UTF-16编码
Unicode、UTF-8 与 UTF-16
在开发的过程中,经常会碰到 Unicode,UTF-8,UTF-16 等等术语,在很多的时候我一直是将它们混为一谈的,不过今天详细的了解了它们之间的不同,作为本章的前言,方便本章内容的理解。
首先 Unicode 表示的是字符集,而 UTF-8,UTF-16 表示的是字符编码。所谓的字符集指的就是字符的集合,Unicode 字符集几乎包含了世界上所有国家的字符,所以 Unicode 又被称为是万国码,如果使用 Unicode 字符集的话,那么可以表示几乎所有国家中的字符(ASCII字符集只包含了英文,而 GBK 字符集包含中日韩等几个国家的字符以及英文字符)。对于每一个字符,Unicode 都为它们对应了一个码点(Code Point),码点从 0 开始计算,例如 \u0000
表示的就是 null
,表示空的意思。
UFT-8,UTF-16 则是将 Unicode 字符集中包含的字符编码为数字存储在计算机中,UTF-8 和 UTF-16 分别对应不同的编码规则。这里详细介绍一下 UTF-16,因为 JavaScript 的编码与之相关。
所谓的 UTF-16 指的是使用 16 位即 2 个字节去表示一个字符,不过随着 Unicode 中包含的字符越来越多,使用 2 个字节已经无法表示 Unicode 中所有的字符,所以实际上 UTF-16 使用 2 个字节或者 4 个字节来表示 Unicode 中的字符,那什么时候用 2 个字节表示,什么时候使用 4 个字节表示?
为了解决这个问题,UTF-16 将字符分为了两部分
- 基本平面:由前 $65536(2^{16})$ 个字符组成,即
\u0000-\uFFFF
- 辅助平面:剩余其它字符组成
对于位于基本平面中的字符使用 2 个字节表示,而对于辅助平面中的字符使用 4 个字节表示。
那 UTF-16 是怎么解码的呢? 如果在解码时发现某 2 个字节表示的数在 \uD800-\uDFFF
之间,则说明这是一个使用 4 个字节表示的字符,如果不在 \uD800-\uDFFF
之间,则说明是基本平面的字符。
但是 JavaScript 并不是使用 UTF-16 进行编码的,而是使用 UCS-2 进行编码的!!!UCS-2 由于发明的比较早,当时 Unicode 中的字符使用 2 个字节即可表示,所以 UCS-2 对于任意的字符都是使用 2 个字节进行编码的。所以在 JavaScript 中有个 bug 就是它不认 4 个字节表示的字符!!!
上面的那段是我从网上看到,然后总结的,但是我有一个疑问,既然 UCS-2 对任意字符都使用 2 个字节进行编码,这不就意味着有的字符无法被编码,而且也不会有 4 个字节表示的字符,我觉得应该是使用 UTF-16 进行编码,但是使用的是 UCS-2 进行解码(仅仅是我的猜测)。
为什么 JavaScript 不使用 UTF-16 进行编码?
因为在 JavaScript 发明的时候,只有 UCS-2 该种编码被提出来了,UTF-16 还没有被提出来。
let text = '𠮷' |
上面的字符串 ‘𠮷’ 明明是一个字符,但是 length 属性却显示有两个字符,这是因为 ‘𠮷’ 是辅助平面上的字符,具有 4 个字节,而 JavaScript 并不认识 4 个字节的字符,会将其解释为两个 2 字节的字符,所以这就是 length 属性为 2 的原因。
并且在使用正则表达式时也有 bug,/^.$/
表示任意只有一个字符的字符串,按照常理来说,/^.$/.test(text)
测试的结果应该是true
,但是最后的结果显示是 false
。
charCodeAt
方法可以获得 Unicode 的 Code Ponit,但是很明显它不能得到 4 字节的 Unicode 字符的 Code Point,只是将这个 4 字节的字符作为两个 2 字节的字符,得到两个 Code Point。
更好的 Uniode 支持
codePointAt()
上面的 charCodeAt()
方法无法得到 4 字节字符的 Code Point,而 ES6 提供了 codePointAt
() 方法,通过该方法可以得到 4 字节字符的 Code Point
let text = '𠮷' |
上面的 134071 是 ‘𠮷’ 的 Code Point。另外通过 codePointAt
方法可以很容易的知道字符是否为 4 字节的字符
function is32BitChar(c){ |
String.fromCodePoint()
String.fromCodePoint()
的作用与 codePointAt()
方法互补,它的作用是根据 Code Point 得到字符
let c = String.fromCodePoint(134071); |
在我的控制台打印出的一个乱码,不知道是不是我控制台编码的问题。
normalize
有的时候,有多个 Code Point 序列对应一个字符
let string1 = '\u00F1'; |
虽然他们表示都是同一个字符,但是因为他们的 Code Point 序列不同,所以直接进行比较时,它们是不同的
let string1 = '\u00F1'; // ñ |
而 normalize 正是来解决这个问题的,它通过将字符转化为标准化的形式,具体见下面的 MDN
正则表达式的 u 标签
如果正则表达式使用了 u 标签,那么他会基于字符匹配而不是 Code Point 进行匹配
let text = '𠮷' |
通过 length 属性,我们得到的并不是字符串的长度,我们可以通过 u 标签计算有多少个 Code Point 从而得到字符串的真实长度
function codePointLength(text) { |
字符串
识别子串
在 ES5 中,我们经常使用 indexOf 方法来识别子串,在 ES6 中新引入了三个方法来帮助我们更加方便的识别子串
- includes
- startsWith
- endsWith
通过方法的名称即可知道方法的作用。这些方法都接收两个参数
- 要识别的子串,substr
- 起始位置,index
对于 index 的含义,includes 和 startsWith 的含义表示以 index 开始识别,而 endsWith 表示以 length - index 结束识别。index 是可选的,如果不传入该参数,默认从字符串的首部开始搜索。
let msg = "Hello World!"; |
如果向 startsWith、endsWith、includes 中传入正则表达式,那么会抛出一个错误。
repeat
repeat 方法接收一个数字参数 n,它的作用是将字符串复制 n 遍然后返回
console.log("hello ".repeat(3)); // "hello hello hello " |
正则表达式
y Flag
ES6 增加了一个 y 标签,该标签表示从 lastIndex 处精确匹配
// 没有使用修饰符 |
当修改包含 y flag 的正则对象的 lastIndex 为 1 后在进行匹配时,并没有匹配到任何东西,因为 index 为 1 后的字符为 ello…,无法与 /hello\d\s?/ 精确匹配,所以不会匹配到任何的东西。
y flag 会存储上次匹配的字符的下一个字符的下标,该值会被存储在lastIndex 中
let stickyPattern= /hello\d\s?/y; |
通过正则对象的 sticky 属性可以知道该对象是否指定了y flag
let pattern = /hello\d/y; |
正则表达式的复制
在 ES5 中可以通过 RegExp 构造函数进行正则表达式的复制
let re1 = /a\db/i, |
但是在构造函数的第二个参数指定了修饰符,那么在 ES5 会报错,而在 ES6 中是可以的,并且新指定的修饰符会覆盖原来的修饰符
let re1 = /a\db/i, |
flags 属性
在 ES5 中想获得正则对象包含哪些修饰符,一般通过解析 toString 得到的字符串
let getFlags = function(re) { |
在 ES6 中为正则对象添加了一个 flags 属性,可以通过 flags 属性来获得正则对象包含的修饰符
let re = /abc/igu; |
模板字符串
ES5 中字符串处理难点:
- 多行字符串
- 字符串格式化
- HTML 转义
ES6 引入模板字符串来解决该问题。
基本语法
使用 ` 号将字符串括起来
let str = `Hello World!`; |
多行字符串
ES6 之前表示多行字符串的写法
let str = "Hello \n\ |
ES6 中更简单的写法
let str = `Hello |
在 `` 中所有的空白字符都会被算作是字符串的一部分。
通过上面的写法,可以写出易于阅读的 HTML 字符串
let html = ` |
模板替换
在 ES5 中一般如下拼接字符串
let name = "Alice"; |
在 ES6 中我们可以使用占位符来方便实现字符串的拼接
let name = "Alice"; |
其中 ${name}
会被本地变量 name 替换,最后形成的字符串就是被替换的字符串。替换的语法就是${}
,其中 {} 中的内容可以是变量名,也可以是表达式
let price = 2.5, |
如果 $ 后面没有 {} 的话,会被当做是正常的美元字符进行输出。
因为模板字符串本身也是表达式,这意味着可以在模板字符串中嵌入模板字符串
let name = "Alice"; |
标签模板
标签对模板字符串进行转换,并返回最终的字符串值。标签放置在模板的前方
let str = tag`Hello World!`; |
其中 tag 就是标签,tag 标签会对模板字符串进行处理,返回的字符串作为最终模板字符串的值
let str = tag`Hello World!`; |
一个标签其实就是一个函数,它接收两个参数
- 由 ${} 分割的模板字符串数组
- ${} 中表达式计算后的结果形成的数组
let price = 2.5, |
在 message 的模板字符串中,有两个 ${}
,所以会被分为三段,这三段的内容形成的数组就是第一个参数的值,两个 ${}
中表达式计算后的值形成的数组是第二个参数的值。
下面使用自定义的标签来模拟模板字符串的默认行为
function defaultTemplate(literals, ...substitutions) { |
标签还可以访问到模板字符串的原始信息,主要指能够访问字符转义。JavaScript 提供了 String.raw 标签以模板访问字符串的原始信息
let str1 = String.raw`Hello\nWorld!`; |
传入标签的 literals 参数有一个 raw 属性,里面保存的就是字符串的原始信息,自定义 raw 标签来模拟 String.raw
function raw(literals, ...substitutions) { |
与 defaultTemplate 不同的就是将 literals[i] 换为了 literals.raw[i]。