Skip to content

Latest commit

 

History

History
299 lines (250 loc) · 12.6 KB

10.正则表达式.md

File metadata and controls

299 lines (250 loc) · 12.6 KB

10.正则表达式

  • 进修正则表达式
  • 编译正则表达式
  • 使用正则表达式进行捕获
  • 使用常见的正则表达式

在主流的 JavaScript 库中, 可以看到开发者大量使用正则表达式解决各种任务。

  • 操作 HTML 节点中的字符串。
  • 使用 CSS 选择器表达式定位部分选择器。
  • 判断一个元素是否具有指定的类名(class)。
  • 输入校验。
  • 其他人物。

通正则表达式需要大量的练习。可以使用网站如 JSBin 很方便地练习。有许多网站致力于测试正则表达式: JavaScript 正则表达式测试egex101。regex101 对于新手是特别有用的网站,可以自动生成正则表达式的解读。

正则表达式进阶

当正则表达式在开发环境是明确的,推荐优先使用字面量语法;当需要在运行时动态创建字符串来构建正则表达式时,则使用构造函数的方式。

除了表达式本身,还可以使用 5 个修饰符:

  • i —— 对大小写不敏感。
  • g —— 查找所有匹配项,在查找到第一个匹配时不会停止,会继续查找下一个匹配项。
  • m —— 允许多行匹配,对获取 textarea 元素的值很有用。
  • y —— 开启粘连匹配。正则表达式执行粘连匹配时试图从最后一个匹配的位置开始。
  • u —— 允许使用 Unicode 点转义符(\u{...})。

术语和操作符

  • 精确匹配:/test/.test('test')
  • 匹配字符集:
    • /[abc]/ 匹配 a、b、c 任意
    • /^abc/ 除 abc 以外
    • /[a-z]/ a-z 之间所有字符
  • 起止符号:
    • /^test/ 匹配字符串的开始(注意,这只是字符的重载,尖角号^还可以表示非)
    • /test$/ 匹配字符串的结束,同时使用 ^$ 表示匹配整个字符串
  • 重复出现:
    • /aaaa/ 匹配 4 个连续的字符 a
    • 匹配任意数量的相同的字符
      • 指定可选字符(可以出现 0 次或 1 次),在字符后添加 ? ,例如,/t?est/可以同时匹配 test 与 est。
      • 指定字符必须出现 1 次或多次,使用 + ,如/t+est/可匹配 test、ttest、 tttest 等。
      • 指定字符出现 0 次或 1 次或多次,使用 * ,如/t*est/匹配 test、ttest、tttest 以及 est。
      • 指定重复次数,使用括号指定重复次数,例如 /a{4}/,匹配 4 个连续的字符 a。
      • 指定循环次数的范围,使用逗号分隔,例如/a{4,10}/匹配 4~10 个连续的字符 a。
      • 指定开放区间,省略第 2 个值,保留逗号。例如/a{4,}/匹配 4 个或更多个连续的字符 a。
      • 些运算符都可以是贪婪的或非贪婪的。默认是贪婪模式,可以匹配所有可能的字符。在运算符后添加?,例如 a+?,使得运算符为非贪婪模式,只进行最小限度的匹配。
  • 预定义字符集
    • \t 水平制表符
    • \b 空格
    • \r 回车符
    • \f 换页符
    • \h 换行符
    • \cA:\cZ 控制字符
    • \u0000:\uFFFF 十六进制 Unicode 码
    • \x00:\xFF 十六进制 ASCII 码
    • . 匹配除换行字符(\n\r\u2028\u2029)之外的任意字符
    • \d 匹配任意十进制数字,等价于[0-9]
    • \D 匹配除了十进制数字外的任意字符,等价于[^0-9]
    • \w 匹配任何字母、数字和下划线,等价于[A-Za-z0-9_]
    • \W 匹配除了字母、数字和下划线之外的字符,等价于[^a-za-z0-9_]
    • \s 匹配任意空白字符(包括空格、制表符、换页符等)
    • \S 匹配除空白字符外的任意字符
    • \b 匹配单词边界
    • \B 匹配非单词边界(单词内部)
  • 分组
    • /(ab)+/ 匹配一个或多个连续的 ab
  • 或操作符(OR)
    • /a|b/ 可以匹配 a 或者 b
    • /(ab)+|(cd)+/ 可以匹配一个或多个 ab 或 cd
  • 反向引用
    • ^([dtn])a\1 匹配任意从 d 或 t 或 n 开始的,连续有 a 的字符串,连续匹配第一个分组中捕获的内容,字符\1表示直到相等才匹配。
    • /<(\w+)>(.+)<\/\1>/ 匹配 <strong>whatever</strong>

编译正则表达式

  • 编译
    • 发生在正则表达式被创建的时期
  • 执行
    • 发生在使用编译之后的正则表 达式进行匹配字符串的时期

在运行时编译一个供稍后使用的正则表达式

<div class="samurai ninja"></div>
<div class="ninja samurai"></div>
<div></div>
<span class="samurai ninja ronin"></span>
<script>
  function findClassInElements(className, type) {
    const elems = document.getElementsByTagName(type || "*");
    const regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
    const results = [];
    for (let i = 0, length = elems.length; i < length; i++) {
      if (regex.test(elems[i].className)) {
        results.push(elems[i]);
      }
    }
    return results;
  }
  console.log(findClassInElements("ninja", "div").length === 2);
  console.log(findClassInElements("ninja", "span").length === 1);
  console.log(findClassInElements("ninja").length === 3);
</script>

捕获匹配的片段

执行简单捕获

<div id="square" style="transform:translateY(15px);"></div>
<script>
  function getTranslateY(elem){
    const transformValue = elem.style.transform;
    if(transformValue){
       const match = transformValue.match(/translateY\(([^\)]+)\)/);
       return match ? match[1] : "";
    }
    return "";
  }
  const square = document.getElementById("square");
  console.log(getTranslateY(square) === "15px");
</script>

使用全局表达式进行匹配

全局匹配与局部匹配查找时的区别:

const html = "<div class='test'><b>Hello</b> <i>world!</i></div>";
const results = html.match(/<(\/?)(\w+)([^>]*?)>/);
assert(results[0] === "<div class='test'>", 'The entire match.');
assert(results[1] === '', 'The (missing) slash.');
assert(results[2] === 'div', 'The tag name.');
assert(results[3] === " class='test'", 'The attributes.');
const all = html.match(/<(\/?)(\w+)([^>]*?)>/g);
assert(all[0] === "<div class='test'>", 'Opening div tag.');
assert(all[1] === '<b>', 'Opening b tag.');
assert(all[2] === '</b>', 'Closing b tag.');
assert(all[3] === '<i>', 'Opening i tag.');
assert(all[4] === '</i>', 'Closing i tag.');
assert(all[5] === '</div>', 'Closing div tag.');

如果捕获结果对我们来说很重要,那么可以在全局匹配中使用正则表达式的 exec 方法。可多次对一个正则表达式调用 exec 方法,每次调用都可以返回下一个匹配的结果。

const html = "<div class='test'><b>Hello</b> <i>world!</i></div>";
const tag = /<(\/?)(\w+)([^>]*?)>/g;
let match,
  num = 0;
while ((match = tag.exec(html)) !== null) {
  assert(match.length === 4, 'Every match finds each tag and 3 captures.');
  num++;
}
assert(num === 6, '3 opening and 3 closing tags found.');

该方法保留前一次调用的结果,这样后续每次调用都可以使用全局匹配。每次调用返回的都是下一次的匹配及捕获结果。通过使用 matchexec 方法,可以精确查找匹配项(和捕获结果)。但是如果希望在正则表达式中引用某个捕获的内容,需要进一步研究。

捕获的引用

使用反向引用匹配 HTML 标记的内容

const html = "<b class='hello'>Hello</b> <i>world!</i>";
const pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;
let match = pattern.exec(html);
assert(
  match[0] === "<b class='hello'>Hello</b>",
  'The entire tag, start to finish.'
);
assert(match[1] === 'b', 'The tag name.');
assert(match[2] === " class='hello'", 'The tag attributes.');
assert(match[3] === 'Hello', 'The contents of the tag.');
match = pattern.exec(html);
assert(match[0] === '<i>world!</i>', 'The entire tag, start to finish.');
assert(match[1] === 'i', 'The tag name.');
assert(match[2] === '', 'The tag attributes.');
assert(match[3] === 'world!', 'The contents of the tag.');

利用函数进行替换

但是 replace 最重要的特性是不仅支持替换值,而且支持替换函数作为参数。当第 2 个参数是函数时,对每一个所匹配到的值都会调用一遍(全局匹配会返回匹配到的全部内容)。

  • 全文匹配。
  • 匹配时的捕获。
  • 在原始字符串匹配的索引。
  • 源字符串。
  • 从函数返回的值作为替换值。

将短横线连接的字符串转换为“驼峰式”字符串

function upper(all, letter) {
  return letter.toUpperCase();
}
console.log(
  'border-bottom-width'.replace(/-(\w)/g, upper) === 'borderBottomWidth'
);

一种查询字符串压缩技术

例如,假设需要将查询字符串转换为符合我们所需格式的字符串,需要将查询字符串

foo=1&foo=2&blah=a&blah=b&foo=3

转换为

foo=1,2,3&blah=a,b"

function compress(source) {
  const keys = {}; // 存储目标 key
  source.replace(/([^=&]+)=([^&]*)/g, function(full, key, value) {
    // 提取键值对信息
    keys[key] = (keys[key] ? keys[key] + ',' : '') + value;
    return '';
  });
  const result = [];
  for (let key in keys) {
    result.push(key + '=' + keys[key]); // 收集key信息
  }
  return result.join('&'); // 使用&符号链接结果
}
assert(
  compress('foo=1&foo=2&blah=a&blah=b&foo=3') === 'foo=1,2,3&blah=a,b',
  'Compression is OK!'
);

示例代码首先声明变量 key,用于存储在查询字符串中匹配的 keyvalue。然后对 source 字符串调用 replace 方法,传入匹配键值对(Hash 结构)的正则表达式,并捕获对应的键值对。同时传入一个函数,接收 3 个参数:fullkeyvalue。捕获的值存储在变量 key 中供后续引用。注意到,返回空字符串是因为我们不关心对 source 字符串的替换,只是使用 replace 方法的副作用,而不关心 replace 方法的执行结果。 当 replace 执行返回时,声明一个数组 result,遍历对象 keys 的属性,将 keys 的属性以及对应的属性值使用“=”符号链接,依次 pushresult 数组中。最后,使用&连接 result 数组中的字符串,并返回结果。

使用正则表达式解决常见的问题

匹配换行

匹配所有字符,包括换行符

const html = '<b>Hello</b>\n<i>world!</i>';
assert(
  /.*/.exec(html)[0] === '<b>Hello</b>',
  "A normal capture doesn't handle endlines."
);
assert(
  /[\S\s]*/.exec(html)[0] === '<b>Hello</b>\n<i>world!</i>',
  'Matching everything with a character set.'
);
assert(
  /(?:.|\s)*/.exec(html)[0] === '<b>Hello</b>\n<i>world!</i>',
  'Using a non-capturing group to match everything.'
);

匹配 Unicode 字符

const text = '\u5FCD\u8005\u30D1\u30EF\u30FC';
const matchAll = /[\w\u0080-\uFFFF_-]+/;
assert(text.match(matchAll), 'Our regexp matches non-ASCII!');

匹配转义字符

在 CSS 选择器中匹配转义字符:

const pattern = /^((\w+)|(\\.))+$/;
const tests = [
  'formUpdate',
  'form\\.update\\.whatever',
  'form\\:update',
  '\\f\\o\\r\\m\\u\\p\\d\\a\\t\\e',
  'form:update'
];
for (let n = 0; n < tests.length; n++) {
  assert(pattern.test(tests[n]), tests[n] + ' is a valid identifier');
}

小结

  • 正则表达式是一个强大的工具,贯穿于现代JavaScript开发中,几乎可用于任意类型的匹配,主要取决于如何在各方面使用正则表达式。本章所介绍的概念能够很好地让你理解高级正则表达式,受益于正则表达式,可以很自信地面对任何具有挑战性的代码。
  • 创建正则表达式可以使用正则表达式字面量(/test/)或正则构造函数 RegExp(new RegExp("test"))。对于在开发环境明确的推荐使用正则字面量,在运行时则推荐使用构造函数。
  • 每个正则都可以使用 5 个标识符:i——大小写敏感,g——全局匹配,m——支持多行匹配,y——支持粘连匹配,u——支持 Unicode 转义。在正则后面添加标志位如/test/ig,或作为构造函数的第 2 个参数传入,如 new RegExp("test", "i")
  • 使用 section 指定一组待匹配的字符。
  • 使用 ^ 表示匹配字符串的起始位置,$表示字符串的结束位置。
  • 使用 ? 表示可选项, + 表示必须出现 1 次或多次,* 表示可以出现 0 次、1 次或多次。
  • 使用 . 匹配任何字符。
  • 使用反斜线 (\) 转义特殊字符。
  • 使用圆括号 () 对多个术语分组,使用竖线 | 表示 或。 通过反斜线+数字如(\1,\2, 等),可以对匹配的字符串进行反向引用。每个字符串可以使用 match 函数,match 函数的传入参数是正则表达式,返回值是匹配到的全部字符串以及全部捕获。使用 replace 函数,可以对固定字符串进行替换。