Javascript中的正则表达式

Stella981
• 阅读 843

正则表达式提供了强大的字符串检索和操作的能力,这些能力在Javascript中有着比其他语言更广泛的应用。对于运行于浏览器环境中的Javascript,HTML文档的DOM操作和样式操作是其主要任务之一,正则表达式的非凡能力正可以应用于此,如:操作DOM节点的内容、解析元素选择器、根据属性值过滤和匹配元素等等。通常总是存在其它方式实现这些操作,但正则表达式可以使我们的实现更加简洁和优雅(一个例子)。

1、Javascript中的正则表达式

ECMAscript内置了标准的对象类型RegExp,提供两个原型方法(exec和test)用以进行正则匹配操作;字符串的包装类型String中,也有原型方法可以使用正则表达式(match、replace、search和split)。下面着重讨论的是正则表达式在Javascript中的使用,正则表达式的基本规则请参见维基百科

2、RegExp对象创建

第一种创建RegExp对象的方式是使用RegExp构造函数:

var rxp = new RegExp("\\s+javascript$","g");

其中第一个参数是描述正则表达式的字符串,第二个参数用来指定匹配模式。

另一种更常用的方式是使用正则表达式字面量:

var rxp = /\s+javascript$/g;

这两种创建方式完全等价。需要注意的是:使用构造函数方式创建RegExp对象时,第一个参数是一个字符串,因此参数中的“\”是需要转义的。字面量中的“\s”在字符串中要写成“\\s”。

正则表达式会在RegExp对象创建时编译,而且每次使用字面量都会创建一个新的RegExp对象(ECMAscript 5):

for (var i = 0; i < 10; i++) {
    /abc/.test("aaabbabccc"); //create an Object every time!
}

var r = /abc/; //create one Object and reuse it
for (var i = 0; i < 10; i++) {
    r.test("aaabbabccc");
}

通常我们应该把正则表达式对象保存在一个变量中以便复用,防止生成过多的对象。

使用构造函数方式创建RegExp对象比使用字面量的一个便利之处,是可以使用动态字符串创建正则表达式,比如检测某个DOM元素class中是否包含某个特定的classname:

function hasClass(className, id) {
    var elem = document.getElementById(id);
        reg = new RegExp("(^|\\s)" + className + "(\\s|$)");
    return reg.test(elem.className);
}

注:使用字面量方式也可以动态构建RegExp对象(eval("/(^|\\s)" + className + "(\\s|$)/")),但需要使用eval函数,因而不建议使用。

3、RegExp构造函数属性

Javascript为RegExp构造函数本身内置了一些属性,用于存储正则表达式执行过程中的相关信息。每次执行了正则表达式操作,这些属性就会发生变化:

属性名

快写形式

说明

input

$_

最后一次匹配的字符串

lastMatch

$&

最后一次的匹配项

lastParen

$+

最后一次的捕获项

leftContext

$`

input字符串中lastMatch之前的内容

rightContext

$'

input字符串中lastMatch之后的内容

此外$1 - $9被用来存储第一至九捕获项:

var html = "Foo<b class='hello'>Hello world!</b>Bar",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/;  
    // 注意pattern中的\1,指的是引用前面部分的第一个捕获项,如果你对正则中的捕获项不了解,请查阅之
var result = pattern.test(html);

console.log(result);               // true
console.log(RegExp.input);         // "Foo<b class='hello'>Hello world!</b>Bar"
console.log(RegExp.lastMatch);     // "<b class='hello'>Hello world!</b>"
console.log(RegExp["$`"]);         // "Foo"
console.log(RegExp["$'"]);         // "Bar"
console.log(RegExp.$1);            // "b"
console.log(RegExp.$2);            // " class='hello'"
console.log(RegExp.$3);            // "Hello world!"

我们将在下面看到正则表达式的捕获功能在Javascript中的应用。

4、RegExp原型对象属性

Javascript中的正则表达式对象都会继承RegExp.prototype中的属性,并根据对象的创建表达式覆盖属性的值。其中的属性包括:source、global、ignoreCase、multiline和lastIndex。

其中source属性是正则表达式的字面量表示的字符串:

var reg1 = /abc(\w+)/,
    reg2 = new RegExp("abc(\\w+)");
console.log(reg1.source);           // "abc(\w+)"
console.log(reg2.source);           // "abc(\w+)"

可以看到,尽管reg2是调用构造函数生成,其source属性依然是字面量形式而非构建时传入的字符串。

global、ignoreCase和multiline属性均布尔值,分别标识正则表达式是否设置了g、i、m模式。(如果你对正则表达式的匹配模式不了解,请查阅之):

var reg1 = /abc(\w+)/gi,
    reg2 = new RegExp("abc(\\w+)", "m");
console.log(reg1.global);           // true
console.log(reg1.ignoreCase);       // true
console.log(reg1.multiline);        // false
console.log(reg2.ignoreCase);       // false

global、ignoreCase和multiline三个属性的Configurable、Enumerable和Writable均为false,也就是说这三个属性在正则对象创建之后便不可再改变。

上面的几个属性在实际应用中并没有太大用处,最后一个属性lastIndex的值表示正则表达式下一次匹配开始的索引位置,我们将在下面看到它的作用。

5、RegExp原型对象方法

RegExp对象定义了两个用于进行模式匹配的方法:exec和test。

exec函数接受一个字符串作为参数,用正则表达式对这个字符串进行模式匹配。exec的返回值是一个数组,数组的第一项是字符串中的第一个匹配项,如果表达式中有捕获项,则按照捕获顺序,把捕获项插入数组;若模式匹配失败,exec函数返回null:

var html = "<b class='hello'>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/;

var match = pattern.exec(html);

console.log(match[0]);      // "<b class='hello'>Hello</b>"
console.log(match[1]);      // "b"
console.log(match[2]);      // " class='hello'"
console.log(match[3]);      // "Hello"

尽管html中有两个pattern的匹配项,但exec函数返回的数组只包含第一个匹配项的内容。返回值match是一个数组,但它被加入了额外的属性:input和index,input的值是进行模式匹配的字符串的内容,index的值是匹配项开始的索引位置:

var html = "Foo<b class='hello'>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/;

var match = pattern.exec(html);

console.log(match.input);      // "Foo<b class='hello'>Hello</b> <i>world!</i>"
console.log(match.index);      // 3

上面所使用的例子中,正则表达式都没有加入匹配模式。当表达式的匹配模式被设置为全局(即加入g标志)时,exec函数的行为会有所不同。全局匹配的涵义是表达式要匹配字符串中的每一个匹配项,而不是普通模式中的只匹配第一个匹配项。看下面的示例,exec函数如何实现全局匹配的:

var html = "<b class='hello'>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;  // NOTE: add a "g"

var match = pattern.exec(html);
console.log(match[0]);      // "<b class='hello'>Hello</b>"
console.log(match[1]);      // "b"
console.log(match[2]);      // " class='hello'"
console.log(match[3]);      // "Hello"

match = pattern.exec(html);
console.log(match[0]);      // "<i>world!</i>"
console.log(match[1]);      // "i"
console.log(match[2]);      // ""
console.log(match[3]);      // "world!"

匹配模式设置为全局之后,我们执行了两次exec函数,第一次执行返回的是第一个匹配项的内容,第二次执行返回的时第二个匹配项的内容。这是如何实现的呢?

还记得前面说过的RegExp对象的lastIndex属性吗?

当RegExp对象的匹配模式设置为全局时,执行exec函数:

1、从lastIndex标示的位置开始对字符串进行匹配,lastIndex值初始为0,即从字符串开始位置进行匹配;

2、若匹配成功,返回匹配项数组,并把lastIndex值设置为匹配项后面一个字符的索引;

3、若匹配失败,返回null,并把lastIndex值设置为0

因此上面的例子中,第二次执行exec函数时,匹配并不是从字符串开始位置进行,而是从上一个匹配项的后面开始的:

var html = "<b class='hello'>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;  // NOTE: add a "g"

console.log(pattern.lastIndex);      // 0
var match = pattern.exec(html);      // start from 0
console.log(match);                  // Array
console.log(match.index);            // match at 0
console.log(pattern.lastIndex);      // 26
match = pattern.exec(html);          // start from 26
console.log(match);                  // Array
console.log(match.index);            // match at 27
console.log(pattern.lastIndex);      // 40
match = pattern.exec(html);          // start from 40
console.log(match);                  // null, match failed
console.log(pattern.lastIndex);      // 0

而当RegExp对象的匹配模式不是全局时,lastIndex的值一直为0,exec无论第几次执行都在字符串起始位置开始,只匹配字符串的第一项。

于是我们可以利用全局模式匹配的行为遍历整个字符串并获取捕获内容:

var html = "<div class='test'><b>Hello</b> <i>world!</i></div>",
    tag = /<(\/?)(\w+)([^>]*?)>/g,
    match;
var num = 0;
while ((match = tag.exec(html)) !== null) {
    console.log(match);  // match every tag start and tag end
    num++;
}
console.log(num);        // 6

全局匹配模式有一个陷阱,就是当使用exec函数成功匹配一个字符串之后去匹配另一个字符串,lastIndex属性并不会初始化为0,而是保留上一次执行exec后设置的值,这可能会引发错误:

var html1 = "<b class='hello'>Hello</b> <i>world!</i>",
    html2 = "<b class='foooo'>Foooo</b> <i>Bar</i>", // why here use "oooo" instead of "oo"?
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;
    
var hello = pattern.exec(html1);
console.log(hello[3]);      // "Hello"
var foo = pattern.exec(html2);
console.log(foo[3]);        // Gotcha!!!  you want "Foooo" but got "Bar"

记得前面创建RegExp对象时,我建议你把它保存在一个变量里面以便复用吗?如果这是一个global模式的表达式,那么你要小心了,重新匹配一个新的字符串时,请确认lastIndex值在你的掌控之中!(使用直接量调用exec函数不会有这个问题,因为每次调用都是一个全新的RegExp对象。负负得正,是吧?^_^)

test行为与exec是等价的,区别是test函数不会返回匹配项的内容,而是返回一个说明匹配是否成功的布尔值。如果只需要验证字符串内容而不需要操作匹配项,那么使用test函数代替exec。

6、String对象原型方法

String对象中的方法,有四个可以使用正则表达式:match、replace、search和split。

search方法是最简单的一个,它接受一个RegExp对象作为参数,返回字符串中第一个匹配的位置。如果传入的参数是一个字符串,则先调用RegExp构造函数将其转换为正则表达式。search方法会忽略全局匹配模式。

match方法的行为与RegExp对象的exec方法类似,它接受一个RegExp对象作为参数,返回一个包含匹配结果的数组,匹配失败时返回null。match方法也会受到表达式匹配模式的影响,全局模式和非全局模式的行为不同:

1、全局模式时,match方法返回的数组元素是每个匹配项;

2、非全局模式,match方法返回的数组元素是第一个匹配项,以及第一个匹配项中的捕获项:

var html = "<b>Hello</b> <i>world!</i>",
    pattern1 = /<(\w+)([^>]*)>(.*?)<\/\1>/g,
    pattern2 = /<(\w+)([^>]*)>(.*?)<\/\1>/;

console.log(html.match(pattern1)); // ["<b>Hello</b>", "<i>world!</i>"]
console.log(html.match(pattern2)); // ["<b>Hello</b>", "b", "", "Hello"]

与RegExp对象的exec方法不同,String对象的match方法不会受到RegExp对象lastIndex属性的影响,match方法总是在字符串的起始位置开始匹配。但match方法以及其它三个方法,会把RegExp对象的lastIndex重置为0。

replace方法是四个方法中最复杂和有趣的一个。replace方法接受两个参数,第一个参数是一个RegExp对象,用于指定字符串中要替换的文本,第二个参数为替换文本的内容,可以是一个字符串或一个函数。replace方法的返回值为执行完替换之后的新字符串。

replace方法也受到匹配模式的影响:第一个参数的正则表达式模式为全局匹配时,替换字符串中所有匹配的位置;非全局模式时,只替换第一个匹配项。

replace方法的第二个参数是一个字符串时,可以使用$符号引用匹配模式中的字符串,类似RegExp构造函数的属性:

$&

当前匹配项

$1 - $99

当前匹配项中的捕获项

$`

字符串中匹配项左边的内容

$'

字符串中匹配项右边的内容

$$

$符号

var html = "<b>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;

html.replace(pattern,"$3");     // "Hello world!"

下面是ECMA-262文档中给出的例子,你能看明白吗?

"$1,$2".replace(/(\$(\d))/g, "$$1-$1$2");  // "$1-$11,$1-$22"

replace方法的第二个参数可以是一个函数,把函数的返回值作为替换内容。函数传入的参数分别是:

1、当前的匹配项

2、当前匹配项中的每一个捕获项作为一个参数

3、当前匹配项在字符串中的索引位置

4、原始字符串

"$1,$2".replace(/(\$(\d))/g, function(){
    console.log(arguments.length);  // 5
    for (var k in arguments) {
        console.log(arguments[k]);
        // 1st loop:
        // arguments[0] : "$1" the match
        // arguments[1] : "$1" 1st capture
        // arguments[2] : "1" 2nd capture
        // arguments[3] : 0 match index
        // arguments[4] : "$1,$2" source string
        // 2nd loop:
        // arguments[0] : "$2" the match
        // arguments[1] : "$2" 1st capture
        // arguments[2] : "2" 2nd capture
        // arguments[3] : 3 match index
        // arguments[4] : "$1,$2" source string
    }
    return "$$1-$1$2";
});
// result: "$$1-$1$2,$$1-$1$2"

作为参数的函数中可以获取所有正则表达式匹配模式的捕获项,这给了我们一种代替使用exec方式遍历操作字符串的方法。尽管replace方法的是用来进行字符串替换的,但我们可以用它来做更多的事情。下面这个例子来自John Resig的《Secrets of the JavaScript Ninja》中,用来对url中的query字段进行归并操作:

function compress(source) {
    var keys = {};                
    source.replace(
        /([^=&]+)=([^&]*)/g,
        function(full, key, value) {            
            keys[key] =
            (keys[key] ? keys[key] + "," : "") + value;
            return "";
        });                
    var result = [];
    for (var key in keys) {
        result.push(key + "=" + keys[key]);
    }                
    return result.join("&");
}
var q = "foo=1&foo=2&blah=a&blah=b&foo=3"
console.log(compress(q));      //foo=1,2,3&blah=a,b

上面的例子中,使用replace方法遍历字符串,捕获数据并传入函数中,在函数中对捕获项进行操作。

一个用replace实现的字符串trim操作:

function trim(str) {
    return (str || "").replace(/^\s+|\s+$/g, "");                
} 
trim("    abcd    ");    // "abcd"

有人专门分析了各种replace方式实现trim的不同性能表现,如果你有兴趣,可以参见这里

split方法用于分割字符串并返回一个数组。split方法可以接受一个RegExp对象作为边界来分割字符串,通常分割字符串不会被插入数组中,除非你设置了捕获项:

var html = "<b>Hello</b> <i>world!</i>";

html.split(/(<[^>]*>)/);  // ["", "<b>", "Hello", "</b>", " ", "<i>", "world!", "</i>", ""]

Javascript中正则表达式比字符串处理有更好的性能。

正则表达式的固有缺点是难以书写和维护,复杂的正则表达式可读性非常差,很多时候它都令人烦躁和心生厌倦。尽管如此,认真学习并把它变成一件好用的工具还是值得的。

这篇文章写了整整一天,希望能对花费时间阅读的人有所帮助,文章参考了以下资料,感谢作者们:

ECMA-262                                 W3C

Javascript高级程序设计(第3版)      Nicholas C.Zakas

Javascript权威指南(第6版)           David Flanagan

Secrets of the JavaScript Ninja    John Resig

JavaScript语言精粹                     Douglas Crockford

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
AJPFX总结关于Java中过滤出字母、数字和中文的正则表达式
1、Java中过滤出字母、数字和中文的正则表达式(1)过滤出字母的正则表达式\^(AZaz)\(2)过滤出数字的正则表达式\^(09)\(3)过滤出中文的正则表达式\^(\\\\u4e00\\\\u9fa5)\(4)过滤出字母、数字和中文的正则表达式\^(azAZ09\\\\u
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这