《深入理解ES6》之字符串和正则表达式

ES6之前,JavaScript字符串是基于16位字符编码(UTF-16)进行构建。每16位序列是一个编码单元,代表一个字符。lengthcharAt()等字符串属性和方法都是基于这种编码单元构造的。

UTF-16码位

Unicode为每一个字符提供了唯一的标识符,又称为码位,它是从0开始的数值。而表示字符的这些数值或者码位,称之为字符编码。

在UTF-16中,前2^16个码位均以16位的编码单元表示,这个范围称作基本多文种平面(BMP)。超出这个范围的码位则要归属于某个辅助平面,因为用16位已经无法表示。为此,UTF-16引入了代理对,规定用两个16位编码单元表示一个码位。

所以,字符串有两种,一种是由一个编码单元16位表示的BMP字符,另一种是有两个编码单元32位表示的辅助平面字符

在ES5中,所有字符串的操作都是基于16位编码单元的BMP字符,如果直接用于32位编码单元的辅助平面字符,得到的结果可能与预期不符。

1
2
3
4
5
6
7
8
var text = "𠮷"; //不是‘吉’
console.log(text.length); //2
console.log(/^.$/.test(text)); //false
console.log(text.charAt(0)) //""
console.log(text.charAt(1)) //""
console.log(text.charCodeAt(0)) //55362
console.log(text.charCodeAt(1)) //57271

𠮷是辅助平面字符,ES5中的字符串操作方法将其视为两个16位字符。

  • text的长度是1,但length属性值为2
  • text被判定为两个字符,因此匹配单一字符的正则失效
  • 前后两个16位编码都不表示任何可打印字符,所以charAt()方法不会返回合法的字符串
  • charCodeAt()方法同样不能正确识别字符,他会返回每个16位编码单元对应的数值

ES6新增的字符串操作方法

ES6新增的字符串方法,完全支持UTF-16(辅助平面字符)

codePointAt()方法

该方法接受编码单元(非字符位置)的位置做为参数,返回字符串中给定位置对应的码位,即一个整数值。

1
2
3
4
5
6
7
8
9
var text = "𠮷a";
console.log(text.charCodeAt(0)); //55362
console.log(text.charCodeAt(1)); //57271
console.log(text.charCodeAt(2)); //97
console.log(text.codePointAt(0)); //134071
console.log(text.codePointAt(1)); //57271
console.log(text.codePointAt(2)); //97

对于BMP字符集中的字符,codePointAtcharCodeAt方法返回的值是相同的。text中第一个字符属于辅助平面字符,包含2个编码单元,length值为3。charCodeAt返回的只是位置0处的第一个编码单元,charPointAt返回的是完整的码位,即使这个码位包含2个编码单元。

检测字符占用的编码单元的数量,可以用charPointAt方法,返回值大于十六进制的上界值FFFF,则一定有两个编码单元来表示。(因为不足以表示所有字符,所以才用辅助平面字符来表示)

1
2
3
4
5
function is32Bit(c) {
return c.codePointAt(0)> 0xFFFF
}
console.log(is32Bit('𠮷')); //true
console.log(is32Bit('a')); //false

String.fromCodePoint()方法

String.fromCodePoint()接收一个码位,返回一个字符

1
console.log(String.fromCodePoint(134071)); //𠮷

normalize()方法

Unicode中,对不同字符进行排序或比较操作,可能它们是等效的。有两种方式可以定义这种关系

  • 规范的等效,两个序列的码位一致
  • 兼容性,两个互相兼容的码位序列看起来不同,但是在特定的情况下可以被互相交换使用。

比如Ǒ(\u01D1),O(\u004F)和ˇ(\u030C)组合,可以互相使用,但从严格意义上来讲,它们不是等效。ES6为字符串提供了一个normalize方法,它可以提供Unicode的标准化形式,该方法接收一个可选字符串参数。

  • 以标准等价方式分解,然后以标准等价方式重组(’NFC’),默认选项
  • 以标准等价方式分解(NFD)
  • 以兼容等价方式分解(’NFKC’)
  • 以兼容等价方式分解,然后易标准等价方式重组(’NFKD’)
    1
    2
    console.log('\u01D1' === '\u004F\u030C'); //false
    console.log('\u01D1'.normalize() === '\u004F\u030C'.normalize()); //true

在对比字符串之前,一定要先把它们标准化为同一格式。

1
2
3
4
5
6
7
8
9
10
11
12
let normalized = values.map(function(text){
return text.normalize()
})
normalized.sort(function(a,b){
if(a < b){
return -1
}else if(a === b){
return 0
}else {
return 1
}
})

values数组中的所有字符都转换成同一种标准格式,因此该数组可以被正确排序

sort方法中的函数,返回值小于0,a在b前面。大于0,a在b后面。等于0,a,b相对位置不变。上面是升序

字符串中的子串识别

  • includes(),字符串中检测到指定文本返回true,否则false
  • startsWith(),字符串起始部分检测到指定文本返回true,否则false
  • endsWith(),字符串结束部分检测到指定文本返回true,否则false
    以上方法,都接受两个参数,第一个参数指定要搜索的文本,第二个参数可选,指定开始搜索的位置的索引值。如果指定第二个参数,includesstartWith方法会从这个索引值的位置开始匹配。endsWith方法则从字符串长度减去这个索引值的位置开始匹配。
    1
    2
    3
    4
    5
    6
    7
    8
    let msg = 'hello world!'
    console.log(msg.startsWith('hello')); //true
    console.log(msg.endsWith('!')); //true
    console.log(msg.includes('o')); //true
    console.log(msg.startsWith('o', 4)); //true
    console.log(msg.endsWith('o',8)) //true
    console.log(msg.includes('o',8)); //false

repeat()方法

重复字符串方法,接受一个number型参数,表示重复次数

1
console.log('hello'.repeat(2)); //'hellohello'

正则表达式变更

u修饰符

正则表达式默认将字符串中的每一个字符按照16位编码单元处理,为了解决这个问题,ES6新增u修饰符。当一个正则表达式添加了u修饰符时,它就从编码单元操作模式切换为字符模式。这样正则表达式就不会把辅助平面字符(代理对)为两个字符。

1
2
3
4
5
let text = '𠮷';
console.log(text.length); //2
console.log(/^.$/.test(text)); //false
console.log(/^.$/u.test(text)); //true

可以利用u修饰符,检测字符串的码位

1
2
3
4
5
6
function codePointLength(text){
let result = text.match(/[\s\S]/gu);
return result? result.length: 0
}
console.log(codePointLength('abc')); //3
console.log(codePointLength('𠮷bc')); //3

检测u修饰符支持

1
2
3
4
5
6
7
8
function hasRegExpU(){
try {
var pattern = new RegExp('.', 'u');
return true
} catch (ex) {
return false
}
}

这个函数使用了RegExp构造函数并传入一个修饰符u作为参数,老式浏览器支持这个语法,如果不支持u修饰符会抛出错误。这样可以避免发生语法错误。

y修饰符

y修饰符的正则表达式称为粘滞正则表达式,他会影响正则表达式搜索过程中的sticky属性,当它在字符串中开始字符匹配时,会通知从正则表达式的lastIndex属性开始进行。如果指定位置未能成功匹配,则停止继续匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let text = 'hello1 hello2 hello3',
pattern = /hello\d\s?/,
result = pattern.exec(text),
globalPattern = /hello\d\s?/g,
globalResult = globalPattern.exec(text),
stickyPattern = /hello\d\s?/y,
stickyResult = stickyPattern.exec(text);
console.log(result[0]); //'hello1 '
console.log(globalResult[0]); //'hello1 '
console.log(stickyResult[0]); //'hello1 '
pattern.lastIndex = 1;
globalPattern.lastIndex = 1;
stickyPattern.lastIndex = 1;
result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);
console.log(result[0]); //'hello1 '
console.log(globalResult[0]); //'hello2 '
console.log(stickyResult[0]); //抛出错误

lastIndex属性改成1时,此时正则表达式从字符串的第二个字符开始匹配。没有修饰符的表达式自动忽略,所以第二个表达式向后匹配到了’hello2 ‘,使用了y修饰符的粘滞正则表达式,由于从第二个字符开始匹配不到相应的字符串,就此终止。所以stickyResultnull

  • 只有调用exec()test()这些正则表达式对象的方法时才会涉及lastIndex属性,调用字符串方法如match()不会触发粘滞行为。
  • 对于粘滞正则表达式,如果使用^匹配字符串开端,只会从字符串的起始位置或者多行模式的首行进行匹配。如果此时lastIndex值不为0,则永远不会匹配到结果

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions

正则表达式复制

1
2
3
4
var reg1 = /ab/i,
reg2 = new RegExp(reg1,'g');
console.log(reg2.toString()); // /ab/g

在ES5环境中,RegExp构造函数,第一个参数为正则表达式时不可以使用第二个参数,ES6中修改了这个行为

flags属性

flags属性用来获取正则表达式的修饰符

1
2
var reg1 = /ab/i;
console.log(reg2.flags); //i

模版字面量

ES6模版字面量语法支持创建领域专用语言(DSL),它比ES5及早起版本中的解决方案更家安全。

基础语法

1
let message = `Hello world!`;

模版字面量用反撇号(`)表示,如果在字符串中使用反撇号,需要用反斜杆(\)进行转义

多行字符串

在ES5中,可以利用一个语法bug,在一个新行的最前方添加反斜杆(\)可以承接上一行代码,来创造多行字符串

1
2
3
4
5
6
7
var message1 = 'Multiline \
string'
var message2 = 'Multiline \n\
string'
console.log(message1); //Multiline string
console.log(message2); //Multiline
//string

反斜杆在此处代表行的延续,而非真正代表新的一行,如果想要输入新的一行,需要手动加入换行符。

ES6之前版本中,通常通过数组活着字符串拼接的方式创建多行字符串

1
2
var message1 = ['Multiline','string'].join('\n');
var message2 = 'Multiline \n' + 'string'

在ES6中的模版字面量,只需在代码中直接换行即可。

1
2
3
4
var message = `Multiline
string`
console.log(message); //Multiline
//string

在反撇号中的所有空白符都输入字符串中的一部分

1
2
3
var message = `Multiline
string` //string前面有4个空格
console.log(message.length); //20

第二行之前的所有空白符都是字符串本身的一部分

字符串占位符

占位符由一个美元符和大括号${}组成,中间可以包含任意的JavaScript表达式。

1
2
3
4
let count = 10,
price = 0.25,
message = `${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message); //10 items cost $2.50.

第一个美元符会原样输出,因为后面没有紧跟一个左括号

模版字面量本身也是JavaScript表达式,所以可以在一个模版中嵌入另一个

1
2
3
4
5
let name = 'Nicholas',
message = `Hello, ${
`my name is ${name}`
}.`;
console.log(message); //Hello, my name is Nicholas.

模版标签

在模版字面量第一个反撇号(`)前方标注的字符串(函数),就是标签。模版标签可以执行模版字面量上的转换,并返回最终的字符串值。

1
let message = tag`hello world`

如示,应用于模版字面量的模版标签是tag

定义标签

标签是一个函数,调用时,第一个参数是一个数组,包含JavaScript解释过后的字面量字符串(模板字符串中那些没有变量替换的部分),之后的所有参数都是每一个占位符的解释值。

标签函数通常使用不定参数特征来定义占位符来简化处理过程

1
2
3
4
5
6
7
8
let count = 10,
price = 0.25,
message = tag`${count} items cost $${(count * price).toFixed(2)}.`;
function tag(literals, ...substitutions){
// console.log(literals); [ '', ' items cost $', '.' ]
// console.log(substitutions); [ 10, '2.50' ]
}

示例中,tag函数,作为一个模版字面量标签,会接受3个参数。literals是一个数组,包含一下元素

  • 第一个占位符前的空字符串(’’)
  • 第一,二个占位符之间的字符串(’ items cost $’)
  • 第二个占位符后的字符串(’.’)

第二个参数是变量count的解释值,传参为10,最后一个是count * price).toFixed(2)的解释值,传参为’2.50’

literals里第一个元素是空字符串,确保literals[0]是字符串始端,substitutions的数量始终比literals少一个,意味着substitutions.length === literals.length-1表达式始终为true

模版标签可以到字符转义被转换成等价字符前的原生字符串,如String.raw()标签

1
2
3
4
5
6
let message1 = `Multiline\nstring`,
message2 = String.raw`Multiline\nstring`;
console.log(message1); //Multiline
//string
console.log(message2); //'Multiline\\nstring'

变量message1中的\n解释为一个新行,变量message2获取的是\n的原生形式\\n

原生字符串信息同样也传入标签模版,标签函数第一个参数,它有一个额外的属性raw,是一个包含每一个字面量的原生等价信息的数组。如literals[0]对应的原生字符串为literals.raw[0]