深入理解ES6之字符创与正则表达式
一、更好的 Unicode 支持
在ES6之前,JS的字符串以16位字符编码(UCS-2)为基础,每个16位序列都是一个码元(code unit),用于表示一个字符。
Unicode 的明确目标是给世界上所有的字符提供全局唯一表示符
二、codePointAt()方法
codePointAt() 可以在给定字符串中按位置提取Unicode代码点。该方法接受的是码元位置而非字符位置,并返回一个整数值,以下示例是《深入理解ES6》的例子,与本人在自己的机器上测试的结果不一致,具体原因不明,如果有人清楚,还请不吝赐教<__ _ __>:
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
codePointAt() 方法的返回值一般与 charCodeAt() 相同,除非操作对象并不是BMP(基本多语言平面,即0x0000-0xFFFF,包含了世界上最常用的字符)字符,在上例中,text字符串的第一个字符不是BMP字符。
判断字符包含了一个还是两个码元,对该字符调用codePointAt() 方法就是最简单的方法,示例如下:
function is32Bit(c){
return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("")) // true
console.log(is32Bit("a")) // false
这是本人测试的结果:
var text = "a";
console.log(text.charCodeAt(0)); // 97
console.log(text.charCodeAt(1)); // NaN
console.log(text.charCodeAt(2)); // NaN
console.log(text.codePointAt(0)); // 97
console.log(text.codePointAt(1)); // undefined
console.log(text.codePointAt(2)); // undefined
function is32Bit(c){
return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("")) // false
console.log(is32Bit("a")) // false
String.fromCodePoint() 方法
借助 String.fromCodePoint() 用给定的代码点来产生包含单个字符的字符串,示例如下:
console.log(String.fromCodePoint(97)); //a
可以将 String.fromCodePoint() 视为 String.fromCharCode() 的完善版本,两者处理BMP字符时会返回相同结果,只有处理BMP范围之外的字符时才会有差异。
三、normalize() 方法
Unicode 另一个有趣之处是,不同的字符在排序或其它一些比较操作中可能会被认为是相同的。有两种方式可以定义这种关联性:第一种是规范相等性(canonical equivalence),意味着两个代码点序列在所有方面都被认为是可互换的。例如,两个字符的组合可以规范等同于另一个字符。第二种关联性是兼容性(compatibility),两个兼容的代码点序列看起来有差别,但在特定条件下可互换使用。
由于这些关联性,文本内容在根本上相同的两个字符串就可以包含不同的代码点序列。
normalize() 方法支持 Unicode 标准形式,该方法接受单个可选的字符串参数,用于指示需要使用系列哪种 Unicode 标准形式:
- Normalization From Canonical Composition(“NFC”),这是默认值;
- Normalization From Canonical Decompositon(“NFD”);
- Normalization From Compatibility Composition(“NFKC”);
- Normalization From Compatibility Decomposition(“NFKD”).
只需记住,当比较字符串时,它们必须被标准化为同一种形式。
四、正则表达式 u 标志
正则表达式假定单个字符使用一个16位的码元来表示,***ES6***为正则表达式定义了用于处理 Unicode 的 u 标志。
当一个正则表达式设置了 u 标志时,它的工作模式将切换到针对字符,而不是针对码元。这意味着正则表达式将不会被字符串中的代理对所混淆,而是会如预期那样工作。
借助 u 标志,可以使用正则表达式判断一个字符串包含多少个代码点,示例如下:
function codePointLength(text) {
var result = text.match(/[/s/S]/gu); // 检查text中的空白字符与非空白字符(使用 [\s\S] 以确保该模式能匹配换行符)
return result ? result.length : 0;
}
console.log(codePointLength("abc")); // 3
console.log(codePointLength("bc")); // 2
判断是否支持 u 标志
使用一个函数来判断是否支持 u 标志是最安全的方式,示例如下:
function hasRegExpU(){
try {
var pattern = new RegExp(".","u");
return true;
}catch(ex){
return false;
}
}
五、识别子字符串的方法
-
includes() 方法,在给定文本存在于字符串中的任意位置时会返回 true ,否则返回 false;
-
startsWith() 方法,在给定文本出现在字符串起始处时返回 true ,否则返回 false ;
-
endsWith() 方法,在给定文本出现在字符串结尾处时返回 true ,否则返回 false 。
以上方法都接受两个参数:需要搜索的文本,以及可选的搜索起始位置索引。示例如下:var 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"));// false console.log(msg.endsWith("world!"));// true console.log(msg.includes("x"));// false console.log(msg.startsWith("o",4));// true console.log(msg.endsWith("o",8)); // true console.log(msg.includes("o",8)); // false
-
repeat() 方法
repeat() 方法接受一个参数作为字符串的重复次数,返回一个将初始字符串重复指定次数的新字符串,示例如下:console.log("x".repeat(3)); // "xxx" console.log("hello".repeat(2)); // "hellohello" console.log("abc".repeat(2)); // "abcabc"
此方法在操纵文本时特别有用,尤其是在需要产生缩进的代码格斯化工具中,示例如下:
// indent 使用了一定数量的空格 var indent = " ".repeat(4),indentLevel = 0; // 每当你增加缩进 var newIndent = indent.repeat(++indentLevel);
第一次调用 repeat() 创建了一个包含四个空格的字符串,而 indentLevel 变量会持续追踪 缩进的级别。此后,你可以仅通过增加 indentLevel 的值来调用 repeat() 方法,便可以改变 空格数量。
六、正则表达式 y 标志
y 标志影响正则表达式搜索时的粘连(sticky)属性,它表示从正则表达式的 lastIndex 属性值的位置开始检索字符串中的匹配字符。如果在该位置没有匹配成功,那么正则表达式将停止检索。
皮一下_:正则表达式非常非常重要,大家一定要熟练掌握。
七、复制正则表达式
var re1 = /ab/i,
re2 = new RegExp(re1);
re2 变量知识 re1 的一个副本。但如果你向 RegExp 构造器传递了第二个参数,那么在 ES5 改代码就无法工作,ES6 修改了这个行为,允许使用第二个参数,并且让它覆盖第一个参数中的标志,示例如下:
var re1 = /ab/i,
// ES5 中会抛出错误,ES6 中可用
re2 = new RegExp(re1,"g");
console.log(re1.toString()); // "/ab/i"
console.log(re2.toString()); // "/ab/g"
console.log(re1.test("ab")) // true
console.log(re2.test("ab")) // true
console.log(re1.test("AB")) // true
console.log(re2.test("AB")) // false
八、flags 属性
flags 属性用于配合 source (ES5用来获取正则表达式的文本)属性,让标志的获取变得更容易。这两个属性均为只有 getter 的原型访问器属性,因此都是只读的。 flags 属性使得检查正则表达式更容易,有助于测试与继承。示例如下:
var re = /ab/g;
console.log(re.source); // "ab"
console.log(re.flags); // "g"
九、模板字面量
ES6 的模板字面量(template literal)提高了创建领域专用语言(domain-specific language,DSL 是被设计用于特定有限目的的编程语言,与通用目的语言如 JavaScript 相反)的语法,与ES5及之前版本的解决方案相比,处理内容可以更安全。ECMAScript wiki 在 template literal strawman 上提供了如下描述:
本方案通过语法糖扩展了 ECMAScript 的语法,允许语言库提供 DSL 以便制作、查询并 操纵来自于其它语言的内容,并且对注入攻击( 如 XSS 、 SQL 注入,等等 )能够免疫 或具有抗性。
不过实际上,模板字面量是 ES6 针对 JS 直到 ES5 依然完全缺失的如下功能的回应:
- 多行字符串:针对多行字符串的形式概念;
- 基本的字符串格式化:将字符串部分替换为已存在的变量值的能力;
- HTML 转义:能转换字符串以便将其安全插入到HTML中的能力。
模板字面量以一种新的方式解决了这些问题,而并未给 JS 已有的字符串添加额外功能。
9.1 基本语法
模板字面量的最简单语法,是使用反引号(`)来包裹普通字符串,示例如下,此示例说明了 message 变量包含的是一个普通的字符串。若想在字符串中包含反引号,只需使用反斜杠(\) 转义即可。
let message = `Hello world!`;
console.log(message);
console.log(typeof message);
console.log(message.length);
//使用反斜杠转义 (\`)
let message2 = `\`Hello\` world!`;
console.log(message2); // "`Hello` world!"
console.log(typeof message2); // "string"
console.log(message2.length); // 14
在模板字面量中无需对双引号或单引号进行转义
9.2 多行字符串
ES6 的模板字面量使多行字符串更易创建,只需在想要的位置包含换行即可,而且它会显示在结果中,示例如下:
let message = `Multiline
string`;
console.log(message); // "Multiline
// string"
console.log(message.length); // 16
如果让多行文本保持合适的缩进对你来说很重要,请考虑将多行模板字面量的第一行空置并在此后进行缩进,示例如下,
let html = `
<div>
<h1>Title</h1>
</div>`.trim(); // trim()方法用于删除字符串的头尾空格,它不会改变原始字符串。
如果你喜欢的话,也可以早模板字面量中使用 \n 来指示换行的插入位置:
let message = `Multiline\nstring`; console.log(message); // "Multiline // string" console.log(message.length); // 16
9.4 制造替换符
模板字面量和普通 JS 字符串的真正区别在于前者的“替换位”。替换位允许你将任何有效的 JS 表达式嵌入到模板字面量中,并将其结果输出为字符串的一部分。
替换位由起始的 " ${ " 与结束的 " } " 来界定,之间允许放入任意的 JS 表达式。示例如下:
let name = "Nicholas",
message = `Hello, ${name}.`;
console.log(message); // "Hello, Nicholas."
替换位 ${name} 会访问本地变量 name ,并将其插入到 message 字符串中。message 变量会立即保留该替换位的结果。
模板字面量能访问到作用域中任意的可访问变量。试图使用未定义的变量会抛出错误,无论是严格模式还是非严格模式。
替换位可替换的不仅仅是简单的变量名,你可以轻易嵌入计算、函数调用,等等。示例如下:
let count = 10,
price = 0.25,
message = `${count} items cost $${(count * price).toFixed(2)}.`; //.toFixed(2) 方法将结果格式化为两位小数
console.log(message);
十、标签化模板
上节学习的模板字面量真正的力量来源于标签化模板。一个模板标签(template tag)能对模板字面量进行转换并返回最终的字符串值,标签在模板的起始处被指定,即在第一个反引号(`) 之前,示例如下:
let message = tag`Hello world`;// tag会被应用到 `Hello world` 模板字面量上的模板标签
10.1 定义标签
一个标签(tag)仅是一个函数,它被调用时接收需要处理的模板字面量数据。标签所接收的数据被划分为独立片段,并且必须将它们组合起来以创建结果。第一个参数是个数组,包含被 JS 解释过的字面量字符串,随后的参数是每个替换位的解释值。
标签函数的参数一般定义为剩余参数形式,以便更容易处理数据,示例如下:
function tag(literals, ...subscriptions) {
// 返回一个字符串
}
为了更好地理解传递给标签的是什么参数,可研究下例:
let count = 10,
price = 0.25,
message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;
分析上述实例。如果你拥有一个名为 passthru() 的函数,该函数将会接收三个参数。首先是一个 literals 数组,包含如下元素:
- 在首个替换位之前的空字符串("");
- 首个替换位与第二个替换位之间的字符串(" items cost $");
- 第二个替换位之后的字符串(".")。
接下来的参数会是 10 ,也就是 count 变量的解释值,它也会成为 substitutions 的数组的第一个元素。最后一个参数则会是 " 2.50 ",即 (count * price).toFixed(2) 的解释值,并且会是 substitutions 数组的第二个元素。
需要注意 literals 的第一个元素是空字符串,以确保 literals[0] 总是字符串的起始部分正如 literals[literals.length - 1] 总是字符串的结尾部分。同时替换位的元素数量也总是比字面量元素少 1,。
可以交替使用 literals 与 substitutions 数组来创建一个结果字符串,示例如下:
function passthru(literals, ...subscriptions) {
let result = "";
// 仅使用substitutions 的元素数量来进行循环
for (let i = 0; i < subscriptions.length; i++) {
result += literals[i];
result += subscriptions[i];
}
result += literals[literals.length - 1];
return result;
}
let count = 10,
price = 0.25,
message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message); // "10 items cost $2.50."
10.2 使用模板字面量中的原始值
模板标签也能访问字符串的原始信息,主要指的是可以访问字符在转义之前的形式。获取原始字符串值的最简单方式是使用内置的 String.raw() 标签,示例如下:
let message1 = `Multiline\nstring`,
message2 = String.raw`Multiline\nstring`;
console.log(message1); // "Multiline
// string"
console.log(message2); // "Multiline\\nstring"
十一、总结
完整的 Unicode 支持允许 JS 以合理的方式处理 UTF-16 字符。通过 codePointAt() 与 String.fromCodePoint() 在代码点和字符之间转换的能力,是字符串操作的一大进步。正则 表达式新增的 u 标志使得直接操作代码点而不是 16 位字符变为可能,而 normalize() 方 法则允许进行更恰当的字符串比较。
ES6 也添加了操作字符串的新方法,允许你更容易识别子字符串,而不用管它在父字符串中 的位置。正则表达式同样引入了许多功能。
模板字面量是 ES6 的一项重要补充,允许你创建领域专用语言( DSL )让字符串的创建更容 易。能将变量直接嵌入到模板字面量中,意味着开发者在组合长字符串与变量时,有了一种 比字符串拼接更为安全的工具。
内置的多行字符串支持,是普通 JS 字符串绝对无法做到的,这使得模板字面量成为凌驾于前 者之上的有用升级。尽管在模板字面量中允许直接使用换行,你依然可以使用 \n 或其它字 符转义序列。
模板标签是创建 DSL 最重要的部分。标签是接收模板字面量片段作为参数的函数,你可以使 用它们来返回合适的字符串。这些数据包括了字面量、等价的原始值以及替换位的值,标签 使用这些信息片段来决定输出。