专栏首页云前端JS正则表达式--从入门到精分

JS正则表达式--从入门到精分

  • 正则表达式是被用来匹配字符串中的字符组合的模式
  • 在JavaScript中,正则表达式也是对象
  • 这种模式可以被用于 RegExp 的 exec 和 test 方法 以及 String 的 match、replace、search 和 split 方法

创建一个正则表达式

  1. 字面量
var re = /http\:\/{2}/;
re.test('http://jobs.douban.com') //true
  1. 构造函数
//用构造函数创建正则往往要对特殊字符双重转义var re = new RegExp('http\\:\\/{2}');
re.test('http://jobs.douban.com') //true

//ES6允许用第二个参数覆盖默认的标志修饰符,ES5则会报错
var re2 = new RegExp(/abc/ig, 'i');
console.log(re2.flags); //i

使用正则表达式的方法

方法

所属

描述

exec

RegExp

在字符串中查找匹配,返回一个特殊数组(未匹配到则返回null)

test

RegExp

在字符串中测试是否匹配,返回true或false

match

String

在字符串中查找匹配,返回一个特殊数组或者在未匹配到时返回null

search

String

在字符串中测试匹配,返回匹配到的位置索引,或者在失败时返回-1

replace

String

在字符串中查找匹配,并且使用替换字符串替换掉匹配到的子字符串

split

String

使用正则或字符串分隔一个字符串,并将分隔后的子字符串存储为数组

常用特殊字符

将其后的特殊字符,转义为字面量

正则表达式标志修饰符

标志

描述

g

全局搜索

i

不区分大小写搜索

m

多行搜索

y

ES6新增,执行“粘性”搜索,匹配从目标字符串的当前位置开始

u

ES6新增,含义为“Unicode模式”,会正确处理四个字节的UTF-16编码(大于\uFFFF)

每个RegExp实例都具有以下属性

global //是否设置了g
ignoreCase //是否设置了i
multiline //是否设置了m
lastIndex //0开始的整数,开始搜索下一个匹配项的位置
source //正则字面量的字符串表示
sticky //ES6新增,表示是否设置了y修饰符
flags //ES6新增,会返回正则表达式的修饰符

几个例子

exec() 返回 null 或 一个特殊数组(有index和input属性)

var exec = /abc\s\"h\d/.exec('helloabc "h2elloabc"'); // ["abc "h2"]
exec.index // 5
exec.input // 'helloabc "h2elloabc"'

//在global情况下
var re = /\del/g, txt = '1ello, 2elabc';
console.log(re.exec(txt)); // ["1el"], index: 0, input: "1ello, 2elabc"
console.log(re.exec(txt)); // ["2el"], index: 7, input: "1ello, 2elabc"

标志修饰符

//特殊字符`?`, 编码U+10437
/?{2}/u.test('??') //true
/?{2}/.test('??') //false

//y和g的区别在于不是紧跟着的粘连模式,相当于隐含的^头部匹配
var str = "applewatch";
var re = /a/g;
re.exec(str); //['a'] index:0
re.exec(str); //['a'] index:6
re.exec(str); //null
var re2 = /a/y;
re2.exec(str); //['a'] index:0
re2.exec(str); //null

调用exec()或test()后,最多9个构造函数属性被自动填充为 RegExp.$1...RegExp.$9,存放括号中匹配的项

var re = /(\de(l.))/g, txt = '1ello, 2elabc';
re.exec(txt);
console.log(RegExp.$1, RegExp.$2);//'1ell', 'll'
re.exec(txt);
console.log(RegExp.$1, RegExp.$2);//'2ela', 'la'

用function作为replace方法第二个参数

  • 可以指定一个函数作为String.prototype.replace()第二个参数
  • 当匹配执行后, 该函数就会执行。 函数的返回值作为替换字符串
  • 该函数的参数为:match, p1, p2, p3..., offset, string
    • match : 匹配的子串
    • p1,p2,p3 ... : 括号分组中匹配的字符串(对应于 1,2,
    • offset : 匹配到的子字符串在原字符串中的偏移量
    • string : 被匹配的原字符串
var str = "吃葡萄不吐putao皮,不吃putao倒吐葡萄皮~";
var str2 = str.replace(/葡萄|putao/g, pt=>{
	console.log(pt);
	return '苹果';
});
//葡萄
//putao
//putao
//葡萄
//str2 == "吃苹果不吐苹果皮,不吃苹果倒吐苹果皮~"

一些常用的正则表达式

//中文、英文、数字及下划线
^[\u4e00-\u9fa5_a-zA-Z0-9]+$//中国邮政编码
[1-9]{1}(\d+){5}//email地址
/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/;//根据useragent判断是否ios
/iP(od|ad|hone)\;?.*\sOS\s([\_0-9]+)///去掉首位空格
str.replace(/(^\s+|\s+$)/g, '')//格式化手机号
tel.replace(/(.{4})/g, '$1 ') //"1332 3385 333"/**
* 获取基于模板的文本值
* @param  {String} tmpl - 文本模板,格式为 'hello{0},world{1}'
* @param  {...String} args - 用于替换的若干参数
* @return {String}
*/
function read_i18n(tmpl, ...args) {
   let rtn = tmpl.substr(0);
   if (args.length) {
       let flagArr = tmpl.match(/\{\d+\}/g); //{1},{0},{2}...
       if (flagArr) {
           for (let i = 0; i < flagArr.length; i++) {
               rtn = rtn.replace(
                   new RegExp("\\{" + i + "\\}", "g"),
                   args[i]
               );
           }
       }
   }
   return rtn;
}read_i18n('hello{0}world{1}', '!', ':)'); // 'hello!world:)'

贪婪模式和懒惰模式

var txt = '<a href=”http://google.com”>谷歌</a><a href=”http://baidu.com”>百度</a>'
var re1 = /\<a (.*?)\<\/a\>/g; //懒惰模式,尽可能少的匹配
var re2 = /\<a (.*)\<\/a\>/g; //贪婪模式,尽可能多的匹配, 区别在不加问号console.log(re1.exec(txt));
//["<a href=”http://google.com”>谷歌</a>", "href=”http://google.com”>谷歌"]console.log(re1.exec(txt));
//["<a href=”http://baidu.com”>百度</a>", "href=”http://baidu.com”>百度"]console.log(re2.exec(txt));
//["<a href=”http://google.com”>谷歌</a><a href=”http://baidu.com”>百度</a>",
//"href=”http://google.com”>谷歌</a><a href=”http://baidu.com”>百度"]console.log(re2.exec(txt));
//null

捕获和非捕获分组

  • 一般的括号被称为捕获分组
    • /(foo) (bar) \1 \2/ 中的 '(foo)' 和 '(bar)' 匹配并记住字符串 "foo bar foo bar" 中前两个单词。
    • 模式中的后向引用 \1 和 \2 匹配字符串的后两个单词。注意 \1、\2、\n 是用在正则表达式的匹配环节。
    • 在正则表达式的替换环节,则要使用像 1、2、n 这样的语法,例如,'bar foo'.replace( /(...) (...)/, '2
  • (?:x)模式的括号被成为非捕获分组,从而不让这个分组被类似 macth exec 这样的函数所获取到
  • var reg = /test(?:\d)+/; var str = 'new test001 test002'; console.log(str.match(reg)); //["test001", index: 4, input: "new test001 test002"]

工作原理

  1. 创建正则表达式后,浏览器检查无误后将其 编译 成本机代码;如果将正则赋给一个变量,可以避免重复执行此步骤
  2. 正则表达式开始工作时, 起始位置 位于字符串的开头或由正则的lastIndex指定;匹配失败后起始位置则重置到最后一次尝试的后一个字符上
  3. 根据目标字符串和正则模版 逐个搜索 ,匹配失败后 回溯(sù) 到该次扫描之前的位置上,并尝试其他可能的分支
  4. 在字符串的当前位置上的所有可能分支都尝试失败后,回到第二步;字符串中每个字符(包括结尾位置)都无法匹配则彻底失败

理解回溯

蒹葭苍苍,白露为霜。所谓伊人,在水一方。溯洄从之,道阻且长。溯游从之,宛在水中央

  • 回溯是正则匹配的基础组成部分,但代价也很昂贵,尽量减少其使用频率,才能编写高效的表达式
  • 正则表达式扫描目标注字符串时,从左到右逐个测试其组成部分,看是否能找到匹配项
  • 对每个量词(诸如*,+?或{2,})和分支都必须决定接下来如何处理
  • 每当正则表达式做决定时,如果有必要的话,都会记住其他选择,以备返回时使用
  • 如果当前选项找不到匹配值,或后面的部分匹配失败,那么正则表达式会回溯到最后一个决策点,然后在剩余的选项中选择一个。这个过程直到最终匹配成功或匹配失败

分支与回溯

/h(ello|appy) hippo/.test("hello there, happy hippo");
  1. 匹配第一个h,成功
  2. 匹配子表达式中的第一个分支,成功
  3. 匹配空格,成功
  4. 匹配t,失败
  5. 尝试2中的另一个分支,第一个字母的匹配都失败了
  6. 回退到第一个字母后面的位置,依次向右挨个字母重复上述匹配
  7. 知道第14个字母h,又匹配成功正则中第一个h
  8. 重复2至5的过程,匹配了子表达式中第二个分支appy
  9. 接下来匹配了整个正则
  10. 得到了匹配的字符串 "happy hippo"

重复与回溯

var re1 = /<p>.*<\/p>/i;
var re2 = /<p>.*?<\/p>/i; //非贪婪(懒惰)
var str1 = "<p>Para.1.</p><img src='smiley.jpg'><p>Para.2.</p><div>Div.</div>";
var str2 = "<p>Para.1.</p>";
  • 参考上面“贪婪模式和懒惰模式”部分的说明
    • 贪婪模式尽可能多的匹配,也就是先吞噬整个剩余字符串,然后从右向左一个个的回溯尝试
    • 懒惰模式尽可能少的匹配,从左向右一个个匹配
  • 对于str1来说,re1能得到比re2更长的字符串,
  • 对于str2来说,re1和re2的结果等效,re1还比re2所用的步骤少一些

回溯失控

失控情况1:不完整的html页面代码

/<html>[\s\S]*?<head>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/
  • 该正则对于完整的html页面工作良好
  • 如果页面中的标签缺失,则正则将大量回溯,可能导致浏览器假死崩溃等
  • 比如遗漏了</html>,则最后一个[\s\S]*?扩展到字符串末尾且无法匹配成功
  • 此时正则不会结束匹配过程,而是回溯到倒数第二个[\s\S]*?,并将其扩展到字符串末尾,尝试 "....</body>...</body>...</html>" 的情况
  • 上一步过程失败后将从右至左以此把其他[\s\S]*?扩展到字符串末尾并失败,从而引发失控
/<html>(?:(?!<head>)[\s\S])*<head>(?:(?!<title>)[\s\S])*<title>(?:(?!<\/title>)[\s\S])*<\/title>(?:(?!<\/head>)[\s\S])*<\/head>(?:(?!<body>)[\s\S])*<body>(?:(?!<\/body>)[\s\S])*<\/body>(?:(?!<\/html>)[\s\S])*<\/html>/
  • 解决方法1: 用 一个包含正向否定查找的非捕获分组 代替了[\s\S]*?
  • 这种结构阻塞了下一个所需的标签,但为每个匹配字符多次正向查找缺乏效率
  • 消除了潜在的回溯失控,且匹配短字符串时良好,但对实际html文件效率没有提高,可能依然要正向预查找上千次
/<html>(?=([\s\S]*?<head>))\1(?=([\s\S]*?<title>))\2(?=([\s\S]*?<\/title>))\3(?=([\s\S]*?<\/head>))\4(?=([\s\S]*?<body>))\5(?=([\s\S]*?<\/body>))\6[\s\S]*?<\/html>/
  • 解决方法2:用 正向肯定查找和匹配环节替换 模拟原子组
  • 该方法更优于‘解决方法1’
  • 匹配环节替换的 后向引用\x 语法在“捕获和非捕获分组”中介绍过
  • 在javascript中不支持其他有些语言中的 原子组(atomic groups),或称 “贪婪子表达式”, 但可以通过 (?=(pattern))\x 模拟;其特点是其中的分组中的任何回溯点都将被丢弃
  • 遗漏了</html>,则最后一个[\s\S]*?扩展到字符串末尾,且整个表达式立即失败,因为没有位置可以回溯了

失控情况2:嵌套量词

/(A+A+)+B/.test('AAAAAAAAAA')
  • 一个典型的例子如上所示
  • 第一个A+匹配全部、第一个匹配9个第二个匹配1个、第一个匹配m个第二个匹配n个再重复分组 等各种情况
  • 最坏的情况下其复杂度为2的n次方,20个长度的A就会回溯百万次,足够造成某些浏览器的崩溃
  • 较好的写法是 /AA+B/
  • 同样,使用模拟原子组,也可以很好的解决问题 /((?=(A+A+))\2)+B/

正则表达式的优化

调试正则时需要考虑的两个因素是准确性和效率:精确匹配需要的文本,并且速度要快

  • 正则的性能因目标文本的不同而差异很大,测试时应尽量使用接近实际的文本
  • 慢往往由过多失败引起,应多考虑让匹配尽快失败的方案
  • 以简单而明确的字元开头,比如^ 等,避免用分支开头
  • 尽量具体化,能用[a-z\r\n]*的就不用.*
  • 分支尽可能少而短,并尽量用速度更快的字符集合取代之;比如用[cb]at代替cat|bat,或用[\s\S]代替(.|\r|\n);常用字符放在集合的前面
  • 在不需要引用括号内文本的时候,尽量用非捕获分组
  • 使用预查找和正确的量词避免回溯失控
  • 将需要重用的正则赋给变量,而不是每次重新创建
  • 将复杂的表达式拆分为简单的片段,并避免一个表达式做太多事情
  • 能用字符串方法直接解决的不要用正则处理

总结图

参考资料

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace
  • http://www.bitscn.com/school/Javascript/201608/718959.html
  • http://www.cnblogs.com/RachelChen/p/5424954.html
  • http://caibaojian.com/es6/regex.html
  • http://keleyi.com/ziliao/js/zzbds.htm
  • http://www.jb51.net/article/31168.htm
  • http://www.cnblogs.com/liuchunmang/archive/2012/02/10/2342066.html
  • http://sentsin.com/web/143_2.html
  • https://mp.weixin.qq.com/s?__biz=MzA5NTM2MTEzNw==&mid=2736711315&idx=2&sn=46f11c955fe647c93036040516fe58eb&chksm=b6aac78d81dd4e9ba0429e863cd4b8187bf711b009a7caf5c7cee9a8212590ebd34fdbdb262d&mpshare=1&scene=1&srcid=0105h36ItjZzXc7u6uW39tlf&key=4656d9815c967482ff88e44f7c9e53cb2e65273a9a271a4f9f316299ac83108bbb9a5ed452a27ff0ce70c20538f67943ba29deaa95c46a4087dffcc62d66c15bed18749de00ee9384d4ab3d4b1ab463c&ascene=0&uin=NzY3MjA4NQ%3D%3D&devicetype=iMac12%2C1+OSX+OSX+10.12.1+build(16B2555)&version=12010210&nettype=WIFI&fontScale=100&pass_ticket=%2FhHEhMrA8QE0TZwyTsLt5VVnBAN9qnxIuiR6v463X6Y%3D

本文分享自微信公众号 - 云前端(fewelife),作者:lua

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-01-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 顺藤摸瓜:用单元测试读懂 vue3 中的 provide/inject

    React Context API 提供了一种 Provider 模式,用以在组件树中的多个任意位置的组件之间共享属性,从而避免必须在多层嵌套的结构中层层传递 ...

    江米小枣
  • [译] 监听第三方 Vue 组件的生命周期钩子

    原文:https://vuedose.tips/listen-to-lifecycle-hooks-on-third-party-vue-js-componen...

    江米小枣
  • 浅谈h5文件上传

    近期的需求中包含了上传头图(图片)和上传菜品(excel文件)的功能,商家可灵活上传使用自己制作的问卷图片,用户评价上传的菜品。

    江米小枣
  • 我真希望你在参加面试前看到这篇文章

    这几天有部分学员在找工作,其中有一个学习很不错的学员,沟通能力也超强。面试七八千的工作,都没问题。但面试薪资在14K以上的岗位,却总是收不到offer。

    致码DevOps
  • 数据分析师面试指南

    经常被问到一个问题,数据分析师或者数据挖掘工程师面试都问什么问题啊?特别是以下几类人群: 1、想转行做数据分析工作的朋友。 2、之前在比较小的公司做数据分析师,...

    小莹莹
  • 关于数据分析师面试,你真的准备好了吗?

    经常被问到一个问题,数据分析师或者数据挖掘工程师面试都问什么问题啊?特别是以下几类人群:

    华章科技
  • 转--golang服务端, 游戏公测时遇到的socket写超时的问题, 也是游戏框架的设计问题

    问题描述: 游戏公测,玩家大概有几百个.运行一小段时间,大概是20分钟左右或最多半个小时,服务端就卡住了. 卡住较长时间,之后又会变正常一小会儿 查问题过程:...

    李海彬
  • 【Go 语言社区】浅析javascript的间隔调用和延时调用

    用 setInterval方法可以以指定的间隔实现循环调用函数,直到clearInterval方法取消循环 用clearInterval方法取消循环时,必须将s...

    李海彬
  • 实战——目标检测与识别

    最近总是有很多入门的朋友问我,我进入计算机视觉这个领域难不难?是不是要学习很多知识?到底哪个方向比较好?。。。。。这些问题其实我也不好回答他们,只能衷心告诉他...

    计算机视觉研究院
  • Redis主从复制

         2.Slave同样可以接收其它Slaves的连接与同步请求,这样可以有效的分载Master的同步压力,因此我们可以将Redis的Replication...

    莫问今朝

扫码关注云+社区

领取腾讯云代金券