30天学习计划 js忍者秘籍 第7章 正则表达式

9.19--9.23

第7章 正则表达式

正则表达式是一个拆分字符串并查询相关信息的过程。

推荐练习网站:

js Bin   jsbin.com

www.regexplanet.com/advanced/javascript/index.html

7.1为什么正则表达式很牛

示例7.1 在字符串中测试特殊模式

//99999-9999验证是否符合这个格式

function isThisAZipCode(candidate){

if(typeof candidate !== 'string' || candidate.length != 10) return false;

for(var n=0; n

var c = candidate[n];

switch(n){

case 0: case 1: case 2: case 3: case 4:

case 6: case 7: case 8: case 9:

if(c<'0' || c >'9') return false;

break;

case 5:

if(c != '-') return false;

break;

}

}

return true;

}

console.log(isThisAZipCode('12345-1234'))

console.log(isThisAZipCode('1245-1234'))

function isThisAZipCode1(candidate){

return /^\d{5}-\d{4}$/.test(candidate)

}

console.log(isThisAZipCode1('12345-1234'))

console.log(isThisAZipCode1('1245-1234'))

同样的字符串验证,用正则表达式看起来更简洁、更优雅。

7.2 正则表达式进阶

7.2.1 正则表达式解释

正则表达式通常被称为一个模式,是一个用简单方式描述或者匹配一系列符合某个句法规则的字符串。表达式本身包含了允许定义这些模式的术语和操作符。

在js中,有两种方法可以创建正则表达式:通过正则表达式字面量,或者通过构造RegExp对象的实例。

var pattern = /test/;

正则字面量是用正斜杠进行界定的。

var pattern = new RegExp('test');

构造一个RegExp实例,将正则作为字符串传入。

这两种格式在pattern变量中创建的正则表达式都是一样的。

在开发过程中,如果正则是已知的,则优先选择字面量语法,而构造器方式则是用于在运行时,通过动态构建字符串来构建正则表达式。

三个标志:

i——让正则表达式不区分大小写,所以/test/i不仅可以匹配'text',还可以匹配‘TEST',‘Test'等

g——匹配模式中的所有实例,而不是默认只匹配第一次出现的结果。

m——允许匹配多个行,比如可以匹配textarea中的值。

这些标志将附加到字面量尾部(例如/test/ig)或者作为RegExp构造器的第二个字符串参数(new RegExp('test','ig'))

7.2.2 术语与操作符

正则表达式,由术语和验证这些术语的操作符组成。

精确匹配

如果一个字符不是特殊字符或操作符,则表示该字符必须在表达式中出现。例如,在/test/正则中,有4个术语,它们表示这些字符必须在一个字符串中出现,才能匹配该模式。

匹配一类字符

可以通过将字符集放到中括号内,来指定该字符集操作符:[abc]。

[abc],是说我们要匹配'a','b','c'中的任何一个字符。它只能匹配候选字符串中的一个字符。

[^abc],匹配除了'a','b','c'以外的任意字符。

[a-m] = [abcdefghijklm],匹配'a'到'm'之间的任何一个小写字母

转义

在正则里,使用反斜杠可以对任意字符进行转义,让被转义字符作为字符本身进行匹配。 例如:\[  匹配 [ ,  \\匹配 \。

匹配开始与匹配结束

插入符号(^),如果作为正则表达式的第一个字符,则表示要从字符串的开头进行匹配。例如/^test/ 只匹配以'test'开头的字符串。

美元符号($),表示该模式必须出现在字符串的结尾。例如 /test$/ 只匹配以'test'结尾的字符串。

同时使用^和$则表明指定的模式必须包含整个候选字符串: /^test$/

重复出现

. 在一个字符后面加一个问号(?),可以定义为该字符是可选的(也就是,可以出现一次或根本不出现)。例如/t?est/可以匹配'test'和'est'。

. 如果一个字符要出现一次或多次,可以使用加号(+)。例如/t+est/可以匹配/'test'/,/'ttest'/,/'tttest'/,而不能匹配/'est'/。

. 如果一个字符要出现零次或多次,可以使用星号(*)。例如/t*est/可以匹配/'test'/,/'ttest'/,/'tttest'/,以及/'est'/。

. 也可以在字符后面的花括号里指定一个数字来表示重复次数。例如,/a{4}/表示匹配含有连续四个'a'字符的字符串。

. 也可以在字符后面的花括号里指定两个数字(用逗号隔开)来表示重复次数区间。例如,/a{4,10}/表示匹配任何含有连续4个或10个'a'字符的字符串。

. 次数区间的第二个值是可选的(但是要保留逗号),其表示一个开区间。例如,/a{4,}/表示匹配任何含有连续4个或多于4个'a'字符的字符串。

这些重复操作符可以是贪婪的或非贪婪的。默认情况下,它们是贪婪的:它们匹配所有的字符组合。在操作符后面加一个问号?字符,如a+?,可以让该表达式编程成为非贪婪的:进行最小限度的匹配。

例如:如果我们对字符串'aaa'进行匹配,/a+/将匹配所有这三个字符,而非贪婪表达式/a+?/则只匹配一个a字符。

预定义字符类

有一些我们想匹配的字符,是不可能用字面量字符来表示的(如回车),还有一些我们可能经常想匹配的字符类(如小数位数或一组空白字符),正则表达式语法提供了很多表示这些字符或常用类的预定义术语。

\t    匹配    水平制表符

\b匹配   空格

\v匹配   垂直制表符

\f匹配   换页符

\r匹配   回车

\n匹配  换行符

\cA: \cZ匹配   控制符,例如\cM 匹配一个 Control-M

\x0000: \xFFFF匹配    十六进制Unicode码

\x00: \xFF匹配    十六进制ASCII码

.匹配    除了换行(\n)之外的任意字符

\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'。

反向引用

正则表达式中最复杂的术语是,在正则中所定义的捕获的反向引用。

在反斜杠后面加一个要引用的捕获数量,该数字从1开始,如\1,\2等。

例如:/^([dtn])a\1/可以任意一个以'd','t','n'开头,且后面跟着一个'a'字符,并且再后面跟着的是和第一个捕获相同字符的字符串。\1匹配的字符需要在执行的时候才能确定。

它在匹配HTML标记的时候非常有用 例如 /<(\w+)>(.+)<\/\1>/

要匹配像'whatever'这样的元素,不使用反向引用,是无法做到的,因为我们无法知道关闭标签和开始标签是否匹配。

7.3 编译正则表达式

正则表达式的两个重要阶段是编译和执行。编译发生在正则表达式第一次被创建的时候,而执行则是发生在我们使用编译过的正则表达式进行字符串匹配的时候。

在编译期间,表达式通过js引擎进行解析,并转换成其内部表示。解析和转换这个过程,在每个正则表达式创建的时候都会发生。

通过对稍后要用的正则表达式进行预定义(因此也预编译),可以获得一些明显的速度提升。

在js中,有两种方式可以创建编译后的正则表达式:通过字面量方式,或通过构造器方式。

示例7.2 创建编译后正则表达式的两种方式

var re1 = /test/i;

var re2 = new RegExp('test','i');

assert(re1.toString() == '/test/i','Verify the contents of the expression.');

assert(re1.test('TesT'),'YES, it\'s case-insensitive.');

assert(re2.test('TesT'),'This one is too.');

assert(re1.toString() == re2.toString(),'The regular expressions are equal.');

assert(re1 != re2, 'But they are different objects.')

正则表达式只编译一次,并将其保存在一个变量中供后续使用,这是一个重要的优化过程。

每个正则表达式都有一个独立的对象表示:每次创建正则表达式,都会为此创建一个新的正则表达式对象。

用构造器创建正则表达式的使用,可以在运行时通过动态创建的字符串构建和编译一个正则表达式。对于构建大量重用的复杂表达式来说,这是非常有用的。

示例 7.3 编译一个稍后使用的运行时正则表达式

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function findClassInElements(className,type){

var elems = document.getElementsByTagName(type || '*');

var regex = new RegExp('(^|\\s)' + className + '(\\s|$)');

var results = [];

for(var i=0,length = elems.length; i

if(regex.test(elems[i].className)){

results.push(elems[i])

}

}

return results;

}

assert(findClassInElements('ninja','div').length == 2,'The right amount of div ninjas was found.');

assert(findClassInElements('ninja','span').length == 1,'The right amount of span ninjas was found.');

assert(findClassInElements('ninja').length == 3,'The right amount of ninjas was found.');

注意,基于传递给函数的样式名称,使用new RegExp()构造器来编译正则表达式,这是一个我们无法用正则字面量实现的实例,因为不知道要搜索的样式名称是什么。

该正则表达式匹配的字符串要以字符串或空格开始,面后跟着指定样式名称,并且紧随其后的是一个空白字符或结束字符串。要注意双反斜杠的使用:\\s。创建带有反斜杠的字面量正则表达式时,只需要提供一个反斜杠即可。但是,由于我们在字符串中写反斜杠,所以需要双反斜杠进行转义。

一旦正则表达式被编译了,就可以利用该表达式的test()方法收集匹配的元素。

7.4 捕获匹配的片段

正则表达式的实用性表现在捕获已匹配的结果上,这样我们便可以在其中进行处理。

7.4.1 执行简单的捕获

示例7.4 捕获嵌入值的简单函数

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function getOpacity(elem){

var filter = elem.style.filter;

return filter ? filter.indexOf('opacity=') >= 0 ? (parseFloat(filter.match(/opacity=([^)]+/)[1])/100)+'':'':elem.style.opacity;

}

assert(getOpacity(document.getElementById('opacity')) == '0.5','The opacity of the element has been obtained.')

match返回的数组的第一个索引的值总是该匹配的完整结果,然后是每个后续捕获结果。

因此,第0个索引的值将是完整的匹配值filter:alpha(opacity=50),下一个匹配则是50.

捕获是由正则表达式中的小括号所定义。

利用String对象的match()方法,使用局部正则表达式(没有全局标记)会返回一个数组,该数组包含了在匹配操作中成功匹配的整个字符串以及其他捕获结果。

7.4.2 用全局表达式进行匹配

当应用全局正则表达式(添加一个g标记)时,返回值依然是一个数组,返回的数组包含了全局匹配结果。在这种情况下,每个匹配的捕获结果是不会返回的。

示例7.5 使用match()进行全局搜索和局部搜索时的不同

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

var html = 'Hello world!';

var results = html.match(/<(\/?)(\w+)([^>]*?)>/);

assert(results[0] == '','The entire match.');

assert(results[1] == '','The (missing) slash.')

assert(results[2] == 'div','The tag name.');

assert(results[3] == ' class="test"','The attributes.');

var all = html.match(/<(\/?)(\w+)([^>]*?)>/g);

assert(all[0] == '','Opening div tag.');

assert(all[1] == '','Opening b tag.')

assert(all[2] == '','Closing b tag.')

assert(all[3] == '','Opening i tag')

assert(all[4] == '','Closing i tag')

assert(all[5] == '','Closing div tag.')

在进行局部正则匹配时,只有一个实例被匹配了,并且该匹配的捕获结果也返回来了;但是在进行全局正则匹配时,返回的却是匹配结果的列表。

如果捕获对我们来说很重要,我们可以使用正则表达式的exec()方法,在全局正则匹配时恢复捕获功能。该方法可以对一个正则表达式进行多次调用,每次调用都可以返回下一个匹配的结果。

示例7.6 使用exec()方法进行捕获和全局搜索

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

var html = 'Hello world!';

var tag = /<(\/?)(\w+)([^>]*?)>/g,match;

var 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.')

在本例中,反复调用了exec()方法,该方法保存了上一次调用的状态,这样每个后续调用就可以继续下去了,直到全局匹配。每个调用返回的都是下一个匹配及其捕获内容。

通过使用match()和exec(),我们总是可以找到想要寻找的精确匹配(及捕捉)。

7.4.3 捕获的引用

有两种方法,可以引用捕获到的匹配结果:一个是自身匹配,一个是替换字符串。

示例7.7 使用反向引用匹配HTML标签内容

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

var html = 'Hello world!';

var pattern = /<(\w+)([^>]*?)>(.*?)<\/\1>/g;

var match = pattern.exec(html);

assert(match[0] == 'Hello','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] == 'world!','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.')

我们使用\1引用了表达式中的第一个捕获,在本例中,该捕获是标签名称。使用这些信息,我们可以匹配相应的结束标签,反向引用到匹配的捕获结果。(如果当前标签有嵌入同名标签,还要再考虑)

还有一个方法可以获得捕获的引用,就是通过调用replace()方法替换字符串的时候。

示例:

assert('fontFamily'.replace(/([A-Z])/g,'-$1').toLowerCase() == 'font-family','Convert the camelCase into dashed notation.')

首先获取的捕获值,在替换字符串中进行了引用(通过$1)。这种方式允许我们指定一个替换字符串,即使是在运行之前还不知道它的值。

7.4.4 没有捕获的分组

小括号有双重责任:不仅要进行分组操作,还可以指定捕获。如果正则表达式中有大量的分组,就会引起很多不必要的捕捉。

要让一组括号不进行结果捕获,正则表达式的语法允许我们在开始括号后加一个?:标记,这就是所谓的被动子表达式。

var pattern = /((?:ninja-)+)sword/;

该表达式只会为外层的括号创建捕获。内层括号被转换为一个被动子表达式。

示例7.8 不带捕获的分组

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

var pattern = /((?:ninja-)+)sword/;

var ninjas = 'ninja-ninja-sword'.match(pattern);

assert(ninjas.length == 2,'Only one capture was returned.')

assert(ninjas[1] == 'ninja-ninja-','Matched both words, without any extra capture.')

通过示例,可以看到,被动子表达式可以阻止不必要的捕获。

在不需要捕获的时候,我们都应该尽可能地使用非捕获(被动)分组,以便让表达式引擎在记忆和返回捕获工作上做更多的工作。

7.5 利用函数进行替换

String对象的replace()方法是一个强大且灵活的方法,将正则表达式作为replace()方法的第一个参数时,导致在该模式的匹配元素上进行替换,而不是在固定字符串上进行替换。

例如:'ABCDEfg'.replace(/[A-Z]/g,'x') 可以让所有的大写字符都替换成'X',结果为XXXXXfg

replace()最强大的特性是可以接受一个函数作为替换值,而不是一个固定的字符串。

当替换值(第二个参数)是一个函数时,每个匹配都会调用该函数(全局搜索会在源字符串中匹配所有的模式实例)并带有一串参数列表。

.匹配的完整文本

.匹配的捕获,一个捕获对应一个参数。

.匹配字符在源字符串中的索引

.源字符串

示例7.9 将中横线字符串转换成驼峰拼写法

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function upper(all,letter){return letter.toUpperCase();}

assert('border-bottom-width'.replace(/-(\w)/g,upper) == 'borderBottomWidth','Camel cased a hyphenated string.')

函数在每次被调用的时候,传入完整的字符串作为第一个参数,捕获结果作为第二个参数。

示例7.10 压缩查询字符串的技术

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

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('&')

}

assert(compress('foo=1&foo=2&blah=a&blah=b&foo=3') == 'foo=1,2,3&blah=a,b','Compression is OK!')

以上示例最为趣的一点是如何使用字符串的replace()方法来遍历一个字符串,而不是一个实际的搜索替换机制。其关键点有两个:传递一个函数作为替换值参数,该函数并不是返回实际的值,而是简单地利用它作 一种搜索手段。

示例代码首先声明一个哈希,用于保存在源查询字符串中找到的键值对。然后在源字符串上调用replace()方法,传入匹配键值对的正则,并捕获匹配的键和值。我们还传入了一个函数,该函数将接收完整匹配值、捕获的键、捕获的值作为参数。这些捕获的值将保存在哈希中,以供稍后进行引用。

在replace()返回后,我们声明一个数组,然后遍历查找到的keys,并且每个结果都聚合到该数组中。最后使用&分隔符,将数组中的所有结果都合并成一个字符串,然后返回该字符串。

我们可以使用String对象的replace()方法作为字符串搜索机制。搜索结果不仅快速,而且简单、有效。

7.6 利用正则表达式解决常见问题

7.6.1 修剪字符串

示例7.11 从字符串中删除空格的常见解决方案

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

function trim(str){

return (str || '').replace(/^\s+|\s+$/g,'');

}

assert(trim(' #id div.class  ') == '#id div.class','Extra whitespace trimmed from a selector string.')

只是调用了一次replace()方法,并传入一个匹配字符串开头和结尾空格的正则来完成这项工作。

另外两种方法

示例7.12 双重替换的修剪实现方式

function trim(str){

return str.replace(/^\s\s*/,'').replace(/\s\s*$/,'');

}

执行两次替换:一个是开头的空格,另一个是结尾的空格。

示例7.13 使用字符串的slice方法剔除字符串尾部空格的方式

function trim(str){

var str = str.replace(/^\s\s*/,'') ,ws = /\s/, i = str.length;

while(ws.test(str.charAt(--i)));

return str.slice(0,i+1);

}

使用一个正则表达式剔除字符串开头的空格,并使用slice操作剔除字符串尾部的空格。

三种trim()实现的性能比较

短字符串       文档

示例7.11        8.7ms      2075.8ms

示例7.128.5ms      3706.7ms

示例7.1313.8ms    169.4ms

大多数js库使用了第一种解决方案

7.6.2 匹配换行符

示例7.14 匹配所有的字符,包括换行符

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

var html = 'Hello\nworld!';

assert(/.*/.exec(html)[0] === 'Hello','A normal capture doesn\'t handle endlines.')

assert(/[\S\s]*/.exec(html)[0] ==='Hello\nworld!','Matching everything with a character set.')

assert(/(?:.|\s)*/.exec(html)[0] === 'Hello\nworld!','Using a non-capturing group to match everything.')

根据代码的简单性,/[\S\s]*/提供的解决方案通常被认为是最佳方案。。

7.6.3 Unicode

示例7.15 匹配 Unicode字符

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

var text = '\u5FCD\u8005\u30D1\u30EF\u30FC';

var matchAll = /[\w\u0080-\uFFFF_-]+/;

assert((text).match(matchAll),'Our regexp matches unicode!')

通过创建一个包含\w的字符类,可以将字符匹配范围扩展到整个Unicode字符集,再加上一套字符代码在128(十六进制为0x80)以上的字符,从而匹配所有的“正常”字符。从128开始,不仅可以匹配所有的Unicode字符,也可以匹配ASCII字符。

通过在\u0080上添加整个Unicode字符集,我们不仅可以匹配字母字符,还可以匹配到所有的Unicode标点符号, 以及其他特殊字符。

7.6.4 转义字符

示例7.16 在CSS选择器中匹配转义字符

test suite

#results .pass{color:green;}

#results .fail{color:red;}

function assert(value,desc){

var li = document.createElement('li');

li.className = value ? 'pass' : 'fail';

li.appendChild(document.createTextNode(desc));

document.getElementById('results').appendChild(li);

}

var pattern = /^((\w+)|(\\.))+$/;  //该正则表达式允许匹配一个单词字符,或一个反斜杠及后面跟随任意字符,或者两者都可以匹配

var tests = ['formUpdate','form\\.update\\.whatever','form\\:update','\\f\\o\\r\\m\\u\\p\\d\\a\\t\\e','form:update'];

for(var n=0; n

assert(pattern.test(tests[n]),tests[n]+' is a valid identifire.')

}

最后一个不能通过,其它都能通过。

这个特殊表达式允许匹配一个单词字符序列,或在一个反斜杠后面跟随任何字符的序列。

推荐阅读更多精彩内容