JavaScript正则表达式入门

正则表达式

正则表达式本身是一种匹配模式,用计算机语言来描述我们需要匹配到的结构

正则表达式语法

正则表达式从匹配形式来说,要么匹配字符,要么匹配位置,以下从分别这两点展开学习

匹配字符

横向模糊匹配

正则匹配到的字符串是不固定的 可以使用量词来指定片段出现的次数,次数会影响到字符串的长度,因为称之为横向模糊匹配

示例

var regex = /ab{2,5}c/g ;   //g 含有一个a,2-5个b,一个c的字符串

var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log( string.match(regex) );

// 匹配结果,只有满足前后位ac,中间只有2-5个b的字符串都匹配到了
// => ["abbc", "abbbc", "abbbbc", "abbbbbc"]

量词

指定需要匹配的字符次数,一些常见的次数表示可以用等价的符号代替,如下图:

纵向模糊匹配

表示同一个位置可以匹配多种可能的字符

示例

var regex = /a[123]b/g;  // 匹配以a开头,以b结尾,中间含有123任意其中一个的字符

var string = "a0b a1b a2b a3b a4b";
console.log( string.match(regex) );

// 匹配结果
// => ["a1b", "a2b", "a3b"]

字符组

常见的纵向模糊匹配集合别名

字符组

字符匹配案例分析

1.匹配日期

匹配 2017-06-10

分析

年 四位数字即可 [0-9]{4}

月,分为0开头和1开头 0[1-9] | 1[0-2]

日,分为0、1、2、3开头 0[1-9] | [12][1-9] | 3[01]

正则

var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
console.log( regex.test("2017-06-10") );
// => true

2.匹配id

分析

id="." 但是由于是贪婪匹配,就会匹配到最后一个双引号为止

id=".*?" 可以使用惰性匹配来解决,但是效率低下

id="[^"]*" 最佳

正则

var regex = /id="[^"]*"/
var string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
// => id="container"

位置匹配

位置在正则中表示相邻字符之间的位置,有以下描点

^

表示字符串开头,多行字符表示行开头

$ 结尾

表示字符串开头,多行字符表示行结尾

下面我们可以把字符串的开头和结尾用'#'代替

单行

var result = "hello".replace(/^|$/g, '#');
console.log(result);

// => "#hello#"

多行

var result = "I\nlove\njavascript".replace(/^|$/gm, '#');
console.log(result);

/*
#I#
#love#
#javascript#
*/

\b 单词边界

\b 是单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置。

var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result);
// => "[#JS#] #Lesson_01#.#mp4#"

\B 非单词边界

var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
console.log(result);
// => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"

(?=p) 先行断言

比如 (?=l),表示 "l" 字符前面的位置,不包括p模式匹配的字符

var result = "hello".replace(/(?=l)/g, '#');
console.log(result);
// => "he#l#lo"

(?!p) 是 (?=p) 取反 先行否定断言

除匹配p模式前面以外的位置

var result = "hello".replace(/(?!l)/g, '#');
console.log(result);
// => "#h#ell#o#"

(?<=p) 后行断言

位置前面要满足匹配p模式,不包括p模式匹配的字符

var result = "hello".replace(/(?<=l)/g, '#');
console.log(result);
// => "hel#l#o"

(?<!p) 后行否定断言

位置前面要满足匹配p模式,除此位置的其余位置

var result = "hello".replace(/(?<=l)/g, '#');
console.log(result);
// => "#h#e#llo#"

位置匹配案例分析

1.数字千位分隔符表示法

比如把 "12345678",变成 "12,345,678"。

分析

这个匹配一看就是匹配3位数字的前面的位置,可以使用先行断言来匹配 ?=\d{3}+, 就可以做到

正则

var result = "12345678".replace(/(?=(\d{3})+$)/g, ',')
console.log(result);
// => "12,345,678"

// 这里尝试3的倍数位数字会发现开头也加上了,
var result = "112345678".replace(/(?=(\d{3})+$)/g, ',')
console.log(result);
// => ",112,345,678"

// 限制此位置不能是开头即可
var regex = /(?!^)(?=(\d{3})+$)/g;
result = "123456789".replace(regex, ',');
console.log(result);
// => "123,456,789"

1.实现字符串trim方法

分析

trim是用来去除字符串首尾空白符,嗯,一读这句话,首尾,那不就是首尾
^ $嘛,至于空白符\s就完事了

正则

function trim(str) {
  return str.replace(/^\s+|\s+$/g, '')
}
console.log(trim(' foobar '))

trim方法是实现首尾去除空白符的,那么新推出的trimStart,trimEnd如何实现呢,嘻嘻,大家可以想想

元字符转义问题

转义这个问题是指一些符号在正则中拥有特殊的含义,比如表示字符串起始位置,那么如何表示这个字符串呢,那么就需要特殊的方法,成为转义,转义只需要在字符前加上\

正则表达式元字符

^、$、.、*、+、?、|、\、/、(、)、[、]、{、}、=、!、:、- ,  

匹配行为-贪婪匹配和惰性匹配

这里的匹配行为由将我们正则转为计算机语言的状态机决定,常见的有DFA和NFA, 目前比较常见的是NFA,JavaScript也是采用NFA实现的正则引擎,NFA一个特点就是会产生回溯行为,生动地来将,它采用类似深度优先搜索思想,遍历可能匹配的字符串,一旦下一次匹配失效,即可回退到前一个状态,听起来很像拿着线球走出迷宫的故事,回溯行为从直观想就能得知会影响效率,在JavaScript正则中,常见的回溯形式为贪婪量词、惰性量词、分支结构,下面会依次介绍

贪婪匹配

最大范围匹配

var regex = /\d{2,5}/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) );
// => ["123", "1234", "12345", "12345"]

示例中会将数字尽可能匹配到,所以看到匹配的数字都是依照空白符分割开来的

惰性匹配 ?

最小匹配范围

var regex = /\d{2,5}?/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) );
// => ["12", "12", "34", "12", "34", "12", "34", "56"]

示例中,加了?的量词之后,正则会在匹配2个数字之后停止

多选分支

子模式任选其一 属于惰性匹配具体形式如下:(p1|p2|p3),其中 p1、p2 和 p3 是子模式,用 |(管道符)分隔,表示其中任何之一。

var regex = /good|nice/g;
var string = "good idea, nice try.";
console.log( string.match(regex) );
// => ["good", "nice"]

括号

在很多语言语法中,括号最常见的是代表优先级,我们看看括号在正则表达式中有什么特殊的用途呢?

产生整体

/ab+/ => a接上至少一个b
如何把量词作用与一个整体 
/(ab)+/

分支结构

表达分支的可能性

表示p1或p2表达式任选其一
var regex = /^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") );
console.log( regex.test("I love Regular Expression") );

分组引用

用于捕获括号匹配的结果

1.提取数据

括号里的匹配字符串可以被直接引用,用以特定的场景

提取年月日

var regex = /(\d{4})-(\d{2})-(\d{2})/g;
var string = "2017-06-12";
console.log( string.match(regex) );
console.log( RegExp.$1 ); // 2017
console.log( RegExp.$2 ); // 06
console.log( RegExp.$3 ); // 12

2.替换

日期更换格式

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result);
// => "06/12/2017"

反向引用

可以引用之前出现的分组结果

有时候需要引用前面匹配的结果,比如说下面要求日期分隔符一致

// 1表示出现的一个分组中的匹配结果
var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false

非捕获括号

不捕获匹配的结果

var regex = /(?:ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) );
// => ["abab", "ab", "ababab"]

括号案例

// 驼峰化
function camelize (str) {
return str.replace(/[-_\s]+(.)?/g, function (match, c) {
return c ? c.toUpperCase() : '';
});
}
console.log( camelize('-moz-transform') );


// 中划线化
function dasherize (str) {
return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
}
console.log( dasherize('MozTransform') );

操作符优先级

正则表达式可视化

emmmm,虽然有优先级,但是有时候还是会看不懂,使用可视化工具帮助我们理解是一个很不错的idea,这里有一个正则可视化网址,在阅读不懂的正则表达式可以助你一臂之力。

正则表达式修饰符

修饰符 描述
g 全局匹配,即找到所有的匹配
y 全局匹配,即找到所有的匹配,但是此匹配要求每个子串是连续下标的
i 忽略字母大小写
m 多行匹配,使用^和$匹配多行时使用

g

默认是找到第一个匹配字符就停止,加了g修饰符就会找到字符串中所有匹配的字符,下面的例子一目了然

var regex = /\d/;
var string = "123";
console.log( string.match(regex) );  // [ '1', index: 0, input: '123' ]

var regex = /\d/g;
var string = "123";
console.log( string.match(regex) );  // [ '1', '2', '3' ]

y

与g行为一致的是,找到全局匹配的子串,但是y有特殊行为,要求每一个匹配的子串起始位置必须是上一个子串的结束位置,看一下下面的例子

var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;

r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]

r1.exec(s) // ["aa"]
r2.exec(s) // null

i

i的含义比较简单

var regex = /\A/i;
var string = "a";
console.log( string.match(regex) ); // [ 'a', index: 0, input: 'a' ]

var regex = /\A/i;
var string = "A";
console.log( string.match(regex) ); // [ 'A', index: 0, input: 'A' ]

m

m就是为了让^和$变成行头和行尾

var regex = /^A$/g;
var string = "A\nA";
console.log( string.match(regex) ); // null

var regex = /^A$/mg;
var string = "A\nA";
console.log( string.match(regex) ); // [ 'A', 'A' ]

正则表达式编程

在我们使用正则表达式匹配之后,JavaScript提供了一些操作给我们使用,下面依次介绍

起始API exec

exec是正则表达式编程中最基本的API,它拥有对字符串匹配,迭代的能力,后续API可以理解为特定场景的exec封装的API,在默认情况下返回第一次匹配的字符串,g修饰符下,每次从上一个匹配结果末端下标开始查找下一个匹配结果

/**
方法返回正则匹配的字符串
* @param string 执行的字符串
* @return {RegExpExecArray | null} 正则执行数组或者null
*/
exec(string: string): RegExpExecArray | null;
let reg = /\d/g;
let s = "123456"
console.log(reg.exec(s)); // [ '1', index: 0, input: '123456' ]
console.log(reg.exec(s)); // [ '2', index: 1, input: '123456' ]

验证

检测目标字符串是否有满足匹配的子串,注意验证是有没有子串,比较常用的是test,它直接返回boolean表示验证结果

 /**
  方法返回一个布尔值表示是否在给定字符串中,存在一个匹配正则的字符串
  * @param string 被测试的字符串
  * @return {boolean} 是否匹配存在匹配子串
*/
test(string: string): boolean;

可以理解成一下代码

// 若有一个或多个匹配,第一次匹配都能匹配到结果,否则返回null
RegExp.prototype.test = function (str) {
  return !!this.exec(str)
}

let reg = /\d/;
let s = "123456"
console.log(reg.test(s)); // true

实例

var regex = /\d/;
var string = "abc123";
console.log( regex.test(string) ); // true

切分

在匹配标志符位置进行切分

/**
  方法按照给定的正则返回分割后的子串数组
  * @param separator 分割器,可以使一个字符串或者一个正则表达式 
  * @param limit 返回结果数组的长度
  * @return {string []} 分割后的字符串数组
  */
split(separator: string | RegExp, limit?: number): string[];

可以理解成以下代码

String.prototype.split = function(reg, limit) {
  let curIndex = -1
  // 此时this为String对象,需要拆包
  let str =  this.valueOf()
  let splitArr = []
  // 执行exec方法
  while(result = reg.exec(str)) {
    let findIndex = result.index 
    // 分隔符相邻
    const isadjoin =  curIndex + 1 === findIndex
    // 分隔符之间的字符
    let splitMiddleStr = str.substring(curIndex + 1, findIndex)
    // 此次分割出来的字符
    let splitedStr = isadjoin ? "" :  splitMiddleStr
    splitArr.push(splitedStr)
    curIndex = findIndex
  }
  return splitArr.slice(limit)
}

实例

var regex = /,/;
var string = "html,css,javascript";
console.log( string.split(regex) );
// => ["html", "css", "javascript"]

var regex = /,/;
var string = "html,css,javascript";
console.log( string.split(regex, 1) );
// => ["html"]

注意点

1.使用正则切分,若正则含有捕获括号, 结果会带有正则匹配部分

var string = "html,css,javascript";
console.log( string.split(/(,)/) );
// =>["html", ",", "css", ",", "javascript"]

提取

匹配后提取部分数据,比较常用的是match

/**
* 方法会以给定的正则去匹配字符串,若匹配成功,则返回匹配数组,否则返回null
* @param regexp 字符串或者正则对象
* @return {RegExpMatchArray | null} 匹配结果数组或者null
*/
match(regexp: string | RegExp): RegExpMatchArray | null;

可以理解成以下代码

var string = "2017.06.27";
var regex1 = /\b(\d+)\b/;
var regex2 = /\b(\d+)\b/g;

String.prototype.match = function (reg) {
  let str = this.valueOf()
  // 是否含有g修饰符
  let isGlobal = reg.global
  let result = []
  let curString = ""
  if(isGlobal) {
    // 返回全部匹配字符串数组
    while(curString = reg.exec(str)) {
      result.push(curString[1])
    }
  }else {
    // 返回第一次匹配的字符串数组
    result = reg.exec(str)
  }
  return result
}
console.log(string.match(regex1)); // [ '2017', '2017', index: 0, input: '2017.06.27' ]
console.log(string.match(regex2)); // [ '2017', '06', '27' ]

实例


var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
console.log( string.match(regex) );
// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]

注意点

1.match会把第一个参数的字符串转成正则

var string = "2017.06.27";
console.log( string.match(".") );
// => ["2", index: 0, input: "2017.06.27"]
//需要修改成下列形式之一
console.log( string.match("\\.") );
console.log( string.match(/\./) );
// => [".", index: 4, input: "2017.06.27"]
// => [".", index: 4, input: "2017.06.27"]

2.match返回值

var string = "2017.06.27";
var regex1 = /\b(\d+)\b/;
var regex2 = /\b(\d+)\b/g;
console.log( string.match(regex1) );
// => ["2017", "2017", index: 0, input: "2017.06.27"]  不带g返回第一个匹配的字符串,分组捕获的内容,整体匹配的第一个下标,输入的目标字符串

console.log( string.match(regex2) );  // 带g返回所有匹配的内容
// => ["2017", "06", "27"]

替换

替换匹配的信息进行处理,使用replace

/**
* 方法按照匹配规则, 使用替换值,将匹配字符串替换为替换值
* @param searchValue 需要匹配的字符串或者正则
* @param replaceValue 替换字符串
* @return {string} 替换后的字符串
*/
replace(searchValue: string | RegExp, replaceValue: string): string;

/**
* 方法按照匹配规则, 
* @param searchValue 需要匹配的字符串或者正则
* @param replacer 替换器
* @return {string} 替换后的字符串
*/
replace(searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string;

可以理解成以下代码,这里仅实现全局替换,让大家了解replace是一个特定场景下的api即可

var string = "2017.06.27";
var regex2 = /\d+/g;

String.prototype.replace = function (reg, replaceValue) {
  // 这里this是String对象,使用valueOf取出字符串值
  let str = this.valueOf()
  // 先用正则分割字符串
  let strArr = str.split(reg)
  // 用replaceValue来填充替换的地方
  str = strArr.join(replaceValue)
  return str
}
console.log(string.replace(regex2, "1")); // 1.1.1

实例

var string = "2017-06-26";
var today = new Date( string.replace(/-/g, "/") );
console.log( today );
// => Mon Jun 26 2017 00:00:00 GMT+0800 (中国标准时间)

注意点

1.当第二个参数是字符串是,有以下特殊字符

2.当第二个参数是函数时,函数传入参数

// 从左往右分别是匹配的字符串,捕获组,匹配字符串的起始位置,输入字符串
[match, $1, $2, index, input]

let  a = "1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) {
  console.log([match, $1, $2, index, input]);
});
// => ["1234", "1", "4", 0, "1234 2345 3456"]
// => ["2345", "2", "5", 5, "1234 2345 3456"]
// => ["3456", "3", "6", 10, "1234 2345 3456"]

3.replace可以嵌套使用

在一些需求里,我们无可避免需要多次正则处理,比如先找到一个整体,再替换这个整体里面的一部分,以缩小影响范围,可以先用replace匹配一次子串,然后再次缩小范围匹配

let domStr = `<div style="font-family: Times, serif, 'Times New Roman'" class="333">
    <div style="font-family: Verdana, Geneva, Tahoma, sans-serif;color: #333">
      <div style="font-family: Verdana, Geneva, Tahoma, sans-serif;color: #333"></div>
    </div>
</div>`
// 匹配style="" 关键是中间部分
let styleRegex = /style="[^"]*"/g  
let result = domStr.replace(styleRegex, function(style) {
  console.log(style);
   let isoffFontFamily = /font\-family\:([^;])*(Times New Roman)+([^;"])*/;
   return style.replace(isoffFontFamily, "");
})

console.log(result);
// <div style="" class="333">
//     <div style="font-family: Verdana, Geneva, Tahoma, sans-serif;color: #333">
//       <div style="font-family: Verdana, Geneva, Tahoma, sans-serif;color: #333"></div>
//     </div>
// </div>

构造函数与实例

1.一般不使用构造函数生成正则表达式,优先使用字面量,除非需要动态生成正则表达式

2.修饰符都有其对应的对象布尔属性表面是否启用

修饰符 实例属性
g global
y sticky
i ingnoreCase
m multiline

3.可以用过source实例属性来查看动态构建的正则表达式结果

var className = "high";
var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
console.log( regex.source )
// => (^|\s)high(\s|$) 即字符串"(^|\\s)high(\\s|$)"

正则表达式的构建

如何针对问题,构建一个合适的正则表达式

法则

1.是否有必要使用正则

人在学习一个新东西之后,很容易陷入到无所不用,正则亦是如此,其实很多时候,我们能够用字符串API解决,就不需要正则出手,以下是一些例子

var string = "2017-07-01";
var regex = /^(\d{4})-(\d{2})-(\d{2})/;
console.log( string.match(regex) );
// => ["2017-07-01", "2017", "07", "01", index: 0, input: "2017-07-01"]

var result = string.split("-");
console.log( result );
// => ["2017", "07", "01"]

在年月日的例子中,我们使用了正则来获取年月日,相比采用分隔符,增加了代码的复杂性。

2.是否有必要严格匹配

一般来说,正则由于其复杂性,复杂度上来,需要严格匹配就很困难,应该结合场景,够用就行,或者可以做一些预处理字符串,使得匹配难度降低。

3.效率

1.尽量使用具体字符组代替通配符,减少回溯

/.*/ 虽然可以匹配任意字符,但是因为贪婪性质,容易出现回溯行为,比如下面的
匹配"abc" 使用/".*"/
// 123"abc"456 

在匹配过程中进行了4次回溯,由于回溯需要保存之前的状态,所以需要额外的空间,另外,回溯行为很直观地可以看出来影响效率,/"[^"]*"/,就可以避免回溯行为

2.独立出确定字符,加快匹配判断速度

/a+/ 
// 如果能确定是比如字符a是存在的,可以改写成一下正则,加快匹配判断速度
/aa+/

3.对多选分支提取公共部分,减少匹配过程中可消除的重复

/^abc|^def/ //修改成 
/^(?:abc|def)/。
// 多选分支是惰性的,在不匹配分支的时候,会尝试其他分支,公共的部分也会进行这个匹配,然而公共的部分没有必要多次匹配,可以提取出来,减少匹配中的重复过程

4.使用非捕获括号

在我们不需要捕获括号中的内容时,可以使用非捕获括号来省掉原本用户保存捕获内容的内存

实践

1.快速找出Vue源码中的所有正则

尝试一

/\/.*\//

// 这会把注释、路径、域名都匹配进去

// /* */  

结果

尝试2

/\/(\S)+\//

// 这里匹配了非空字符串,而且必须至少出现一次

// /* */  

结果

一下子少了很多,但是仍然会匹配到不是正则的 /ggg/ 可能只是路径中的一部分,但是我们需要检查的量已经可以接受了。

posted @ 2019-08-18 21:42  兴趣使然的Geek  阅读(847)  评论(0编辑  收藏  举报