《JavaScript 闯关记》之正则表达式

原创 2016年10月17日 22:34:30

由于本课程的核心是 JavaScript,所以本文着重讲解了「正则表达式」在 JavaScript 中的用法,并未深入「正则表达式」的具体细节。如果您尚不了解「正则表达式」,强烈推荐您先学习 正则表达式30分钟入门教程 http://deerchao.net/tutorials/regex/regex.htm 之后,再进行本课程的学习。

正则表达式(regular expression)是一个描述字符模式的对象,使用正则表达式可以进行强大的模式匹配和文本检索与替换功能。JavaScript 的正则表达式语法是 Perl5 的正则表达式语法的大型子集,所以对于有 Perl 编程经验的程序员来说,学习 JavaScript 中的正则表达式是小菜一碟。

正则表达式的定义

JavaScript 中的正则表达式用 RegExp 对象表示,可以使用 RegExp() 构造函数来创建 RegExp 对象,不过 RegExp 对象更多是通过字面量的语法来创建,使用下面类似 Perl 的语法,就可以创建一个正则表达式。例如:

// 推荐写法
var expression = / pattern / flags ;

// 不推荐写法
var expression = new RegExp(pattern, flags);

其中的模式(pattern)部分,是包含在一对斜杠 / 之间的字符,可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找以及反向引用。每个正则表达式都可带有一或多个标志(flags),用以标明正则表达式的行为。正则表达式的匹配模式支持下列3个标志。

  • g:表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即停止;
  • i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写;
  • m:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。

因此,一个正则表达式就是一个模式与上述3个标志的组合体。不同组合产生不同结果,如下面的例子所示。

// 匹配字符串中所有"at"的实例
var pattern1 = /at/g;

// 匹配第一个"bat"或"cat",不区分大小写
var pattern2 = /[bc]at/i;

// 匹配所有以"at"结尾的3个字符的组合,不区分大小写
var pattern3 = /.at/gi;

与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。正则表达式中的元字符包括:

( [ { \ ^ $ | ) ? * + . ] }

这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,就必须对它们进行转义。下面给出几个例子。

// 匹配第一个"bat"或"cat",不区分大小写
var pattern1 = /[bc]at/i;

// 匹配第一个" [bc]at",不区分大小写
var pattern2 = /\[bc\]at/i;

// 匹配所有以"at"结尾的3个字符的组合,不区分大小写
var pattern3 = /.at/gi;

// 匹配所有".at",不区分大小写
var pattern4 = /\.at/gi;

RegExp 实例属性

RegExp 的每个实例都具有下列属性,通过这些属性可以取得有关模式的各种信息。

  • global:布尔值,表示是否设置了 g 标志。
  • ignoreCase:布尔值,表示是否设置了 i 标志。
  • lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从0算起。
  • multiline:布尔值,表示是否设置了 m 标志。
  • source:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。

通过这些属性可以获知一个正则表达式的各方面信息,但却没有多大用处,因为这些信息全都包含在模式声明中。例如:

var pattern1 = /\[bc\]at/i;
console.log(pattern1.global);         // false
console.log(pattern1.ignoreCase);     // true
console.log(pattern1.multiline);      // false
console.log(pattern1.lastIndex);      // 0
console.log(pattern1.source);         // "\[bc\]at"

var pattern2 = new RegExp("\\[bc\\]at", "i");
console.log(pattern2.global);         // false
console.log(pattern2.ignoreCase);     // true
console.log(pattern2.multiline);      // false
console.log(pattern2.lastIndex);      // 0
console.log(pattern2.source);         // "\[bc\]at"

我们注意到,尽管第一个模式使用的是字面量,第二个模式使用了 RegExp 构造函数,但它们的 source 属性是相同的。可见,source 属性保存的是规范形式的字符串,即字面量形式所用的字符串。

RegExp 实例方法

RegExp 对象的主要方法是 exec(),该方法是专门为捕获组而设计的。exec() 接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回 null。返回的数组虽然是 Array 的实例,但包含两个额外的属性:indexinput。其中,index 表示匹配项在字符串中的位置,而 input 表示应用正则表达式的字符串。在数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串(如果模式中没有捕获组,则该数组只包含一项)。请看下面的例子。

var text = "mom and dad and baby";
var pattern = /mom( and dad( and baby)?)?/gi;

var matches = pattern.exec(text);
console.log(matches.index);     // 0
console.log(matches.input);     // "mom and dad and baby"
console.log(matches[0]);        // "mom and dad and baby"
console.log(matches[1]);        // " and dad and baby"
console.log(matches[2]);        // " and baby"

这个例子中的模式包含两个捕获组。最内部的捕获组匹配 "and baby",而包含它的捕获组匹配 "and dad" 或者 "and dad and baby"。当把字符串传入 exec() 方法中之后,发现了一个匹配项。因为整个字符串本身与模式匹配,所以返回的数组 matchsindex 属性值为 0。数组中的第一项是匹配的整个字符串,第二项包含与第一个捕获组匹配的内容,第三项包含与第二个捕获组匹配的内容。

对于 exec() 方法而言,即使在模式中设置了全局标志 g,它每次也只会返回一个匹配项。在不设置全局标志的情况下,在同一个字符串上多次调用 exec() 将始终返回第一个匹配项的信息。而在设置全局标志的情况下,每次调用 exec() 则都会在字符串中继续查找新匹配项,如下面的例子所示。

var text = "cat, bat, sat, fat";

var pattern1 = /.at/;

// 非全局模式,第一次匹配
var matches = pattern1.exec(text);
console.log(matches.index);        // 0
console.log(matches[0]);           // cat
console.log(pattern1.lastIndex);   // 0

// 非全局模式,第二次匹配
matches = pattern1.exec(text);
console.log(matches.index);        // 0
console.log(matches[0]);           // cat
console.log(pattern1.lastIndex);   // 0

var pattern2 = /.at/g;

// 全局模式,第一次匹配
var matches = pattern2.exec(text);
console.log(matches.index);        // 0
console.log(matches[0]);           // cat
console.log(pattern2.lastIndex);   // 0

// 全局模式,第二次匹配
matches = pattern2.exec(text);
console.log(matches.index);        // 5
console.log(matches[0]);           // bat
console.log(pattern2.lastIndex);   // 8

这个例子中的第一个模式 pattern1 不是全局模式,因此每次调用 exec() 返回的都是第一个匹配项 "cat"。而第二个模式 pattern2 是全局模式,因此每次调用 exec() 都会返回字符串中的下一个匹配项,直至搜索到字符串末尾为止。此外,还应该注意模式的 lastIndex 属性的变化情况。在全局匹配模式下,lastIndex 的值在每次调用 exec() 后都会增加,而在非全局模式下则始终保持不变。

IE 的 JavaScript 实现在 lastIndex 属性上存在偏差,即使在非全局模式下,lastIndex 属性每次也会变化。

正则表达式的第二个方法是 test(),它接受一个字符串参数。在模式与该参数匹配的情况下返回 true;否则,返回 false。在只想知道目标字符串与某个模式是否匹配,但不需要知道其文本内容的情况下,使用这个方法非常方便。因此,test() 方法经常被用在 if 语句中,如下面的例子所示。

var text = "000-00-0000";
var pattern = /\d{3}-\d{2}-\d{4}/;

if (pattern.test(text)){
    console.log("The pattern was matched.");
}

在这个例子中,我们使用正则表达式来测试了一个数字序列。如果输入的文本与模式匹配,则显示一条消息。这种用法经常出现在验证用户输入的情况下,因为我们只想知道输入是不是有效,至于它为什么无效就无关紧要了。

RegExp 实例继承的 toLocaleString()toString() 方法都会返回正则表达式的字面量,与创建正则表达式的方式无关。例如:

var pattern = new RegExp("\\[bc\\]at", "gi");
console.log(pattern.toString());             // /\[bc\]at/gi
console.log(pattern.toLocaleString());       // /\[bc\]at/gi

即使上例中的模式是通过调用 RegExp 构造函数创建的,但 toLocaleString()toString() 方法仍然会像它是以字面量形式创建的一样显示其字符串表示。

RegExp 构造函数属性

RegExp 构造函数包含一些属性(这些属性在其他语言中被看成是静态属性)。这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问它们。换句话说,这些属性分别有一个长属性名和一个短属性名(Opera是例外,它不支持短属性名)。下表列出了RegExp构造函数的属性。

|长属性名 | 短属性名 | 说明 |
| — | — | — |
input | |OperalastMatch|& | 最近一次的匹配项。Opera未实现此属性。
lastParen | +|OperaleftContext|` | input字符串中lastMatch之前的文本。
multiline | |使IEOperarightContext|’ | Input字符串中lastMatch之后的文本。

使用这些属性可以从 exec()test() 执行的操作中提取出更具体的信息。请看下面的例子。

var text = "this has been a short summer";
var pattern = /(.)hort/g;

/*
 * 注意:Internet Explorer 不支持 multiline 属性
 * Opera 不支持 input、lastMatch、lastParen 和 multiline 属性
 */
if (pattern.test(text)){
    console.log(RegExp.input);          // this has been a short summer
    console.log(RegExp.leftContext);    // this has been a
    console.log(RegExp.rightContext);   // summer
    console.log(RegExp.lastMatch);      // short
    console.log(RegExp.lastParen);      // s
    console.log(RegExp.multiline);      // false
}

如前所述,例子使用的长属性名都可以用相应的短属性名来代替。只不过,由于这些短属性名大都不是有效的 JavaScript 标识符,因此必须通过方括号语法来访问它们,如下所示。

var text = "this has been a short summer";
var pattern = /(.)hort/g;

/*
 * 注意:Internet Explorer 不支持 multiline 属性
 * Opera 不支持 input、lastMatch、lastParen 和 multiline 属性
 */
if (pattern.test(text)){
    console.log(RegExp.$_);      // this has been a short summer
    console.log(RegExp["$`"]);   // this has been a
    console.log(RegExp["$'"]);   // summer
    console.log(RegExp["$&"]);   // short
    console.log(RegExp["$+"]);   // s
    console.log(RegExp["$*"]);   // false
}

除了上面介绍的几个属性之外,还有多达9个用于存储捕获组的构造函数属性。访问这些属性的语法是 RegExp.$1RegExp.$2RegExp.$9,分别用于存储第一、第二…第九个匹配的捕获组。在调用 exec()test() 方法时,这些属性会被自动填充。然后,我们就可以像下面这样来使用它们。

var text = "this has been a short summer";
var pattern = /(..)or(.)/g;

if (pattern.test(text)){
    console.log(RegExp.$1);  // sh
    console.log(RegExp.$2);  // t
}

这里创建了一个包含两个捕获组的模式,并用该模式测试了一个字符串。即使 test() 方法只返回一个布尔值,但 RegExp 构造函数的属性 $1$2 也会被匹配相应捕获组的字符串自动填充。

模式的局限性

尽管 JavaScript 中的正则表达式功能还是比较完备的,但仍然缺少某些语言(特别是 Perl)所支持的高级正则表达式特性。下面列出了 JavaScript 正则表达式所不支持的特性。

  • 匹配字符串开始和结尾的\A和\Z锚
  • 向后查找(lookbehind)
  • 并集和交集类
  • 原子组(atomic grouping)
  • Unicode支持(单个字符除外,如\uFFFF)
  • 命名的捕获组
  • s(single,单行)和x(free-spacing,无间隔)匹配模式
  • 条件匹配
  • 正则表达式注释

即使存在这些限制,JavaScript 正则表达式仍然是非常强大的,能够帮我们完成绝大多数模式匹配任务。

关卡

按要求完成下列常用的正则表达式。

// 挑战一:数字
var pattern1 = null;  // 补全该正则表达式
console.log(pattern1.test('123')); // true
console.log(pattern1.test('abc')); // false
// 挑战二:3位的数字
var pattern2 = null;  // 补全该正则表达式
console.log(pattern2.test('123'));  // true
console.log(pattern2.test('1234')); // false
// 挑战三:至少3位的数字
var pattern3 = null;  // 补全该正则表达式
console.log(pattern3.test('1234')); // true
console.log(pattern3.test('12'));   // false
// 挑战四:3-5位的数字
var pattern4 = null;  // 补全该正则表达式
console.log(pattern4.test('1234')); // true
console.log(pattern4.test('1'));    // false
// 挑战五:由26个英文字母组成的字符串
var pattern5 = null;  // 补全该正则表达式
console.log(pattern5.test('abc'));  // true
console.log(pattern5.test('1abc')); // false
// 挑战六:由数字和26个英文字母组成的字符串
var pattern6 = null;  // 补全该正则表达式
console.log(pattern6.test('1abc'));  // true
console.log(pattern6.test('_abc'));  // false
// 挑战七:日期格式:年-月-日
var pattern7 = null;  // 补全该正则表达式
console.log(pattern7.test('2016-08-20'));  // true
console.log(pattern7.test('2016/08/20'));  // false
// 挑战八:时间格式:小时:分钟, 24小时制
var pattern8 = null;  // 补全该正则表达式
console.log(pattern8.test('13:45'));  // true
console.log(pattern8.test('13点45')); // false
// 挑战九:中国大陆身份证号,15位或18位
var pattern9 = null;  // 补全该正则表达式
console.log(pattern9.test('4223222199901090033'));  // true
console.log(pattern9.test('asdfasdfasfasdf1234'));  // false

更多

关注微信公众号「劼哥舍」回复「答案」,获取关卡详解。
关注 https://github.com/stone0090/javascript-lessons,获取最新动态。

版权声明:本文为博主原创文章,欢迎转载,但需注明出处。

《JavaScript 闯关记》之简介

JavaScript 是面向 Web 的编程语言,绝大多数现代网站都使用了 JavaScript,并且所有的现代 Web 浏览器(电脑,手机,平板)均包含了 JavaScript 解释器。 这使得 J...
  • shi0090
  • shi0090
  • 2016年07月24日 01:20
  • 1535

《JavaScript 闯关记》之初探

初探当学习一门新的编程语言的时候,应该边学边做,反复演练以加深理解。因此,你需要一个 JavaScript 解释器。幸运的是,每一个 Web 浏览器都包含一个 JavaScript 解释器。可以通过在...
  • shi0090
  • shi0090
  • 2016年07月31日 14:28
  • 487

《JavaScript 闯关记》之 DOM(下)

Element 类型除了 Document 类型之外,Element 类型就要算是 Web 编程中最常用的类型了。Element 类型用于表现 XML 或 HTML 元素,提供了对元素标签名、子节点及...
  • shi0090
  • shi0090
  • 2016年11月22日 20:15
  • 887

《JavaScript 闯关记》之原型及原型链

原型链是一种机制,指的是 JavaScript 每个对象都有一个内置的 __proto__ 属性指向创建它的构造函数的 prototype(原型)属性。原型链的作用是为了实现对象的继承,要理解原型链,...
  • shi0090
  • shi0090
  • 2016年12月20日 23:51
  • 9852

《JavaScript 闯关记》之函数

函数是一段代码,它只定义一次,但可以被执行或调用任意次。在 JavaScript 里,函数即对象,程序可以随意操控它们。比如,可以把函数赋值给变量,或者作为参数传递给其他函数,也可以给它们设置属性,甚...
  • shi0090
  • shi0090
  • 2016年10月12日 21:40
  • 771

《JavaScript 闯关记》之基本包装类型

为了便于操作基本类型值,JavaScript 还提供了3个特殊的引用类型:Boolean、Number 和 String。实际上,每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象...
  • shi0090
  • shi0090
  • 2016年10月26日 22:47
  • 1335

《JavaScript 闯关记》之单体内置对象

ECMA-262 对内置对象的定义是「由 JavaScript 实现提供的、不依赖于宿主环境的对象,这些对象在 JavaScript 程序执行之前就已经存在了」。意思就是说,开发人员不必显式地实例化内...
  • shi0090
  • shi0090
  • 2016年11月01日 22:21
  • 823

《JavaScript 闯关记》之垃圾回收和内存管理

JavaScript 具有自动垃圾收集机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。而在 C 和 C++ 之类的语言中,开发人员的一项基本...
  • shi0090
  • shi0090
  • 2016年12月29日 20:45
  • 2436

《JavaScript 闯关记》之 BOM

ECMAScript 是 JavaScript 的核心,但如果要在 Web 中使用 JavaScript,那么 BOM(浏览器对象模型)则无疑才是真正的核心。BOM 提供了很多对象,用于访问浏览器的功...
  • shi0090
  • shi0090
  • 2016年11月07日 21:55
  • 1421

《JavaScript 闯关记》之数组

数组是值的有序集合。每个值叫做一个元素,而每个元素在数组中有一个位置,以数字表示,称为索引。JavaScript 数组是无类型的,数组元素可以是任意类型,并且同一个数组中的不同元素也可能有不同的类型。...
  • shi0090
  • shi0090
  • 2016年10月07日 22:57
  • 1538
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:《JavaScript 闯关记》之正则表达式
举报原因:
原因补充:

(最多只允许输入30个字)