diff --git a/9.md b/9.md index d21b9b1..5289bf8 100644 --- a/9.md +++ b/9.md @@ -1,18 +1,22 @@ ## 九、正则表达式 -程序设计工具技术的发展与传播方式是在混乱中不断进化。在此过程中获胜的往往不是优雅或杰出的一方,而是那些瞄准主流市场,并能够填补市场需求的工具技术——比如我们可以将一种技术与另一种成功的技术集成在一起。 +> Yuan-Ma said, 'When you cut against the grain of the wood, much strength is needed. When you program against the grain of the problem, much code is needed.' +> +> Master Yuan-Ma,《The Book of Programming》 -本章将会讨论正则表达式(regular expression)这种工具。正则表达式是一种描述字符串数据模式的方法。在JavaScript和其他语言或工具中,正则表达式往往是一种小巧且独立的语言。 +程序设计工具技术的发展与传播方式是在混乱中不断进化。在此过程中获胜的往往不是优雅或杰出的一方,而是那些瞄准主流市场,并能够填补市场需求的,或者碰巧与另一种成功的技术集成在一起的工具技术。 + +本章将会讨论正则表达式(regular expression)这种工具。正则表达式是一种描述字符串数据模式的方法。它们形成了一种小而独立的语言,也是 JavaScript 和许多其他语言和系统的一部分。 正则表达式虽然不易理解,但是功能非常强大。正则表达式的语法有点诡异,JavaScript提供的程序设计接口也不太易用。但正则表达式的确是检查、处理字符串的强力工具。如果读者能够正确理解正则表达式,将会成为更高效的程序员。 -### 9.1 创建正则表达式 +### 创建正则表达式 正则表达式是一种对象类型。我们可以使用两种方法来构造正则表达式:一是使用RegExp构造函数构造一个正则表达式对象;二是使用斜杠(/)字符将模式包围起来,生成一个字面值。 ``` -var re1 = new RegExp("abc"); -var re2 = /abc/; +let re1 = new RegExp("abc"); +let re2 = /abc/; ``` 这两个正则表达式对象都表示相同的模式:字符a后紧跟一个b,接着紧跟一个c。 @@ -22,12 +26,10 @@ var re2 = /abc/; 第二种写法将模式写在斜杠之间,处理反斜杠的方式与第一种方法略有差别。首先,由于斜杠会结束整个模式,因此模式中包含斜杠时,需在斜杠前加上反斜杠。此外,如果反斜杠不是特殊字符代码(比如\n)的一部分,则会保留反斜杠,不像字符串中会将其忽略,也不会改变模式的含义。一些字符,比如问号、加号在正则表达式中有特殊含义,如果你想要表示其字符本身,需要在字符前加上反斜杠。 ``` -var eighteenPlus = /eighteen\+/; +let eighteenPlus = /eighteen\+/; ``` -编写正则表达式时,如果想知道哪些字符需要使用反斜杠进行转义,则需要知道所有具有特殊含义的字符。眼下这是不切实际的,因此当不确定字符是否具有特殊含义,而且不是字母、数字和空格时,只要在这些字符前加上反斜杠即可。 - -### 9.2 匹配测试 +### 匹配测试 正则表达式对象有许多方法。其中最简单的就是test方法。test方法接受用户传递的字符串,并返回一个布尔值,表示字符串中是否包含能与表达式模式匹配的字符串。 @@ -40,9 +42,9 @@ console.log(/abc/.test("abxde")); 不包含特殊字符的正则表达式简单地表示一个字符序列。如果使用test测试字符串时,字符串中某处出现abc(不一定在开头),则返回true。 -### 9.3 匹配字符集 +### 字符集 -我们也可调用indexOf来找出字符串中是否包含abc。正则表达式不仅可以用来匹配字符序列,还可以用其表达一些更复杂的模式。 +我们也可调用indexOf来找出字符串中是否包含abc。正则表达式允许我们表达一些更复杂的模式。 假如我们想匹配任意数字。在正则表达式中,我们可以将一组字符放在两个方括号之间,该表达式可以匹配方括号中的任意字符。 @@ -57,7 +59,7 @@ console.log(/[0-9]/.test("in 1992")); 我们可以在方括号中的两个字符间插入连字符(–),来指定一个字符范围,范围内的字符顺序由字符Unicode代码决定。在Unicode字符顺序中,0到9是从左到右彼此相邻的(代码从48到57),因此[0-9]覆盖了这一范围内的所有字符,也就是说可以匹配任意数字。 -在正则表达式中,许多通用字符组都有其内置的快捷写法。数字的是其中之一:\d表示的意义与[0-9]相同。 +许多常见字符组都有自己的内置简写。 数字就是其中之一:`\ d`与`[0-9]`表示相同的东西。 \d任意数字符号 @@ -76,28 +78,28 @@ console.log(/[0-9]/.test("in 1992")); 因此你可以使用下面的表达式匹配类似于30-01-200315:20这样的日期数字格式: ``` -var dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/; +let dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/; console.log(dateTime.test("30-01-2003 15:20")); // → true console.log(dateTime.test("30-jan-2003 15:20")); // → false ``` -这个表达式看起来是不是非常可怕?该表达式中加入了太多反斜杠,影响读者的理解,使得读者难以揣摩表达式实际想要表达的模式。稍后我们会看到一个稍加改进的版本。 +这个表达式看起来是不是非常糟糕?该表达式中一半都是反斜杠,影响读者的理解,使得读者难以揣摩表达式实际想要表达的模式。稍后我们会看到一个稍加改进的版本。 -我们也可以将这些反斜杠代码用在方括号中。例如,[\d.]匹配任意数字或一个句号。但这里需要注意方括号中的句号会失去其特殊含义。其他特殊字符也是如此,比如+。 +我们也可以将这些反斜杠代码用在方括号中。例如,[\d.]匹配任意数字或一个句号。但是方括号中的句号会失去其特殊含义。其他特殊字符也是如此,比如+。 你可以在左方括号后添加脱字符(^)来排除(Invert)某个字符集,即表示不匹配这组字符中的任何字符。 ``` -var notBinary = /[^01]/; +let notBinary = /[^01]/; console.log(notBinary.test("1100100010100110")); // → false console.log(notBinary.test("1100100010200110")); // → true ``` -### 9.4 部分模式重复 +### 部分模式重复 现在我们已经知道如何匹配一个数字。如果我们想匹配一个整数(一个或多个数字的序列),该如何处理呢? @@ -114,12 +116,12 @@ console.log(/'\d*'/.test("''")); // → true ``` -星号(*)有的含义与加号类似,但是可以匹配模式不存在的情况。在正则表达式的元素后添加星号并不会导致正则表达式停止匹配该元素后面的字符。只有正则表达式无法找到可以匹配的文本时才会考虑匹配该元素从未出现的情况。 +星号(*)拥有类似含义,但是可以匹配模式不存在的情况。在正则表达式的元素后添加星号并不会导致正则表达式停止匹配该元素后面的字符。只有正则表达式无法找到可以匹配的文本时才会考虑匹配该元素从未出现的情况。 元素后面跟一个问号表示这部分模式“可选”,即模式可能出现0次或1次。下面的例子可以匹配neighbour(u出现1次),也可以匹配neighbor(u没有出现)。 ``` -var neighbor = /neighbou?r/; +let neighbor = /neighbou?r/; console.log(neighbor.test("neighbour")); // → true console.log(neighbor.test("neighbor")); @@ -128,36 +130,36 @@ console.log(neighbor.test("neighbor")); 我们可以使用花括号准确指明某个模式的出现次数。例如,在某个元素后加上{4},则该模式需要出现且只能出现4次。也可以使用花括号指定一个范围:比如{2,4}表示该元素至少出现2次,至多出现4次。 -这里给出另一个版本的正则表达式,可以匹配日期、月份、小时,每个数字都可以是一位或两位数字。这种形式更易于理解。 +这里给出另一个版本的正则表达式,可以匹配日期、月份、小时,每个数字都可以是一位或两位数字。这种形式更易于解释。 ``` -var dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/; +let dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/; console.log(dateTime.test("30-1-2003 8:45")); // → true ``` 花括号中也可以省略逗号任意一侧的数字,表示不限制这一侧的数量。因此{,5}表示0到5次,而{5.}表示至少五次。 -### 9.5 子表达式分组 +### 子表达式分组 -如果想一次性对多个元素使用*或者+,那么你需要使用圆括号将这些元素包围起来,创建一个分组。这些操作符会将包围在括号中的那部分正则表达式当作一个整体处理。 +为了一次性对多个元素使用*或者+,那么你必须使用圆括号,创建一个分组。对于后面的操作符来说,圆括号里的表达式算作单个元素。 ``` -var cartoonCrying = /boo+(hoo+)+/i; +let cartoonCrying = /boo+(hoo+)+/i; console.log(cartoonCrying.test("Boohoooohoohooo")); // → true ``` 第一个和第二个+字符分别作用于boo与hoo的o字符,而第三个+字符则作用于整个元组(hoo+),可以匹配hoo+这种正则表达式出现一次及一次以上的情况。 -上述示例中表达式末尾的i表示正则表达式不区分大小写,虽然模式中使用小写字母,但可以匹配输入字符串中的大写字母B。 +示例中表达式末尾的i表示正则表达式不区分大小写,虽然模式中使用小写字母,但可以匹配输入字符串中的大写字母B。 -### 9.6 匹配和分组 +### 匹配和分组 test方法是匹配正则表达式最简单的方法。该方法只负责判断字符串是否与某个模式匹配。正则表达式还有一个exec(执行,execute)方法,如果无法匹配模式则返回null,否则返回一个表示匹配字符串信息的对象。 ``` -var match = /\d+/.exec("one two 100"); +let match = /\d+/.exec("one two 100"); console.log(match); // → ["100"] console.log(match.index); @@ -176,7 +178,7 @@ console.log("one two 100".match(/\d+/)); 若正则表达式包含使用圆括号包围的子表达式分组,与这些分组匹配的文本也会出现在数组中。第一个元素是与整个模式匹配的字符串,其后是与第一个分组匹配的部分字符串(表达式中第一次出现左圆括号的那部分),然后是第二个分组。 ``` -var quotedText = /'([^']*)'/; +let quotedText = /'([^']*)'/; console.log(quotedText.exec("she said 'hello'")); // → ["'hello'", "hello"] ``` @@ -192,15 +194,15 @@ console.log(/(\d)+/.exec("123")); 分组是提取部分字符串的实用特性。如果我们不只是想验证字符串中是否包含日期,还想将字符串中的日期字符串提取出来,并将其转换成等价的日期对象,那么我们可以使用圆括号包围那些匹配数字的模式字符串,并直接将日期从exec的结果中提取出来。 -不过,我们暂且先讨论另一个话题——在JavaScript中较为推荐的存储日期和时间的方法。 +不过,我们暂且先讨论另一个话题——在 JavaScript 中存储日期和时间的内建方法。 -### 9.7 日期类型 +### 日期类 -JavaScript提供了用于表示日期的标准对象类型,我们甚至可以用其表示时间点。该类型名为Date。如果使用new创建一个Date对象,你会得到当前的日期和时间。 +JavaScript提供了用于表示日期的标准类,我们甚至可以用其表示时间点。该类型名为Date。如果使用new创建一个Date对象,你会得到当前的日期和时间。 ``` console.log(new Date()); -// → Wed Dec 04 2013 14:24:57 GMT+0100 (CET) +// → Mon Nov 13 2017 16:19:11 GMT+0100 (CET) ``` 你也可以创建表示特定时间的对象。 @@ -216,7 +218,7 @@ JavaScript中约定是:使用从0开始的数字表示月份(因此使用11 构造函数的后四个参数(小时、分钟、秒、毫秒)是可选的,如果用户没有指定这些参数,则参数的值默认为0。 -我们使用从1970年开始流逝的毫秒数表示时间戳,如果在1970年前,则使用负数(遵循由“Unix时间”设定的约定,Unix时间就是在1970左右创造出来的)。Date对象的getTime方法返回这种时间戳。读者可以想象到这个数字会有多大。 +时间戳存储为 UTC 时区中 1970 年以来的毫秒数。 这遵循一个由“Unix 时间”设定的约定,该约定是在那个时候发明的。 您可以对 1970 年以前的时间使用负数。 日期对象上的`getTime`方法返回这个数字。 你可以想象它会很大。 ``` console.log(new Date(2013, 11, 19).getTime()); @@ -227,25 +229,25 @@ console.log(new Date(1387407600000)); 如果你为Date构造函数指定了一个参数,构造函数会将该参数看成毫秒数。你可以创建一个新的Date对象,并调用getTime方法,或调用Date.now()函数来获取当前时间对应的毫秒数。 -Date对象提供了一些方法来提取时间中的某些数值,比如getFullYear、getMonth、getDate、getHours、getMinutes、getSeconds。该对象还有一个getYear方法,会返回使用两位数字表示的年份(比如93或14),但很少用到。 +`Date`对象提供了一些方法来提取时间中的某些数值,比如`getFullYear`、`getMonth`、`getDate`、`getHours`、`getMinutes`、`getSeconds`。除了`getFullYear`之外该对象还有一个`getYear`方法,会返回使用两位数字表示的年份(比如 93 或 14),但很少用到。 -我们可以在希望捕获的那部分模式字符串两边加上圆括号,轻松地通过字符串创建对应的Date对象。 +通过在希望捕获的那部分模式字符串两边加上圆括号,我们可以从字符串中创建对应的Date对象。 ``` -function findDate(string) { - var dateTime = /(\d{1,2})-(\d{1,2})-(\d{4})/; - var match = dateTime.exec(string); - return new Date(Number(match[3]), - Number(match[2]) - 1, - Number(match[1])); +function getDate(string) { + let [_, day, month, year] = + /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string); + return new Date(year, month - 1, day); } -console.log(findDate("30-1-2003")); +console.log(getDate("30-1-2003")); // → Thu Jan 30 2003 00:00:00 GMT+0100 (CET) ``` -### 9.8 单词和字符串边界 +`_`(下划线)绑定被忽略,并且只用于跳过由`exec`返回的数组中的,完整匹配元素。 -不幸的是,findDate会从字符串“100-1-30000”中提取出一个无意义的日期——00-1-3000。正则表达式可以从字符串中的任何位置开始匹配,在我们的例子中,它从第二个字符开始匹配,到倒数第二个字符为止。 +### 单词和字符串边界 + +不幸的是,`getDate`会从字符串`"100-1-30000"`中提取出一个无意义的日期——`00-1-3000`。正则表达式可以从字符串中的任何位置开始匹配,在我们的例子中,它从第二个字符开始匹配,到倒数第二个字符为止。 如果我们想要强制匹配整个字符串,可以使用^标记和$标记。脱字符表示输入字符串起始位置,美元符号表示字符串结束位置。因此/^\d+$/可以匹配整个由一个或多个数字组成的字符串,/^!/匹配任何以感叹号开头的字符串,而/x^/不匹配任何字符串(字符串起始位置之前不可能有字符x)。 @@ -258,33 +260,35 @@ console.log(/\bcat\b/.test("concatenate")); // → false ``` -这里需要注意,边界标记并不表示实际的字符,只在强制正则表达式满足模式中的条件时才进行匹配。 +这里需要注意,边界标记并不匹配实际的字符,只在强制正则表达式满足模式中的条件时才进行匹配。 -### 9.9 选项模式 +### 选项模式 假如我们不仅想知道文本中是否包含数字,还想知道数字之后是否跟着一个单词(pig、cow或chicken)或其复数形式。 那么我们可以编写三个正则表达式并轮流测试,但还有一种更好的方式。管道符号(|)表示从其左侧的模式和右侧的模式任意选择一个进行匹配。因此代码如下所示。 ``` -var animalCount = /\b\d+ (pig|cow|chicken)s?\b/; +let animalCount = /\b\d+ (pig|cow|chicken)s?\b/; console.log(animalCount.test("15 pigs")); // → true console.log(animalCount.test("15 pigchickens")); // → false ``` -小括号可用于限制管道符号选择的模式范围,而且你可以连续使用多个管道符号,表示从多于两个模式中选择一个模式进行匹配。 +小括号可用于限制管道符号选择的模式范围,而且你可以连续使用多个管道符号,表示从多于两个模式中选择一个备选项进行匹配。 -### 9.10 匹配原理 +### 匹配原理 -我们可以将正则表达式看成流程图。下图描述了前文示例中匹配牲畜的表达式。 +从概念上讲,当你使用`exec`或`test`时,正则表达式引擎在你的字符串中寻找匹配,通过首先从字符串的开头匹配表达式,然后从第二个字符匹配表达式,直到它找到匹配或达到字符串的末尾。 它会返回找到的第一个匹配,或者根本找不到任何匹配。 + +为了进行实际的匹配,引擎会像处理流程图一样处理正则表达式。 这是上例中用于家畜表达式的图表: ![](../Images/00302.jpeg) -如果我们可以找到一条从图表左侧通往图表右侧的路径,则可以说“表达式匹配了字符串”。我们保存在字符串中的当前位置,每移动通过一个盒子,就验证当前位置之后的部分字符串是否与该盒子匹配。 +如果我们可以找到一条从图表左侧通往图表右侧的路径,则可以说“表达式产生了匹配”。我们保存在字符串中的当前位置,每移动通过一个盒子,就验证当前位置之后的部分字符串是否与该盒子匹配。 -因此,如果我们尝试使用我们的正则表达式匹配“the 3 pigs”,大致会以如下的过程通过流程图: +因此,如果我们尝试从位置 4 匹配`"the 3 pigs"`,大致会以如下的过程通过流程图: + 在位置4,有一个单词边界,因此我们通过第一个盒子。 @@ -298,15 +302,15 @@ console.log(animalCount.test("15 pigchickens")); + 我们在位置10(字符串结尾),只能匹配单词边界。而字符串结尾可以看成一个单词边界,因此我们通过最后一个盒子,成功匹配字符串。 -从概念上讲,正则表达式引擎在字符串中寻找匹配部分时,会从字符串起始位置尝试匹配。在本例中,有一个单词边界,因此引擎通过第一个盒子,但这里没有数字,因此在第二个盒子这里失败。接着引擎移动到字符串中第二个字符,开始新一轮匹配过程……以此类推,直到其找到一个匹配部分,或到达字符串结尾(确定没有任何匹配项)为止。 +### 回溯 -### 9.11 回溯 +正则表达式/\b([01]+b|\d+|[\da-f]h)\b/可以匹配三种字符串:以b结尾的二进制数字,以h结尾的十六进制数字(即以16为进制,字母a到f表示数字10到15),或者没有后缀字符的常规十进制数字。这是对应的图表。 -正则表达式/\b([01]+b|\d+|[\da-f]h)\b/可以匹配三种字符串:以b结尾的二进制数字、没有后缀字符的十进制数字、以h结尾的十六进制数字(即以16为基,字母a到f表示数字10到15)。这是对应的图表。 +![]() 当匹配该表达式时,常常会发生一种情况:输入的字符串进入上方(二进制)分支的匹配过程,但输入中并不包含二进制数字。我们以匹配字符串“103”为例,匹配过程只有遇到字符3时才知道进入了错误分支。该字符串匹配我们给出的表达式,但没有匹配目前应当处于的分支。 -因此匹配器需要“回溯”。进入一个分支时,匹配器会记住当前位置(在本例中,是在字符串起始,刚刚通过图中第一个表示边界的盒子),因此若当前分支无法匹配,可以回退并尝试另一条分支。遇到字符3时,字符串“103”会开始尝试匹配十进制数字那条分支。由于这条分支可以匹配,因此匹配器最后的会返回十进制数的匹配信息。 +因此匹配器执行“回溯”。进入一个分支时,匹配器会记住当前位置(在本例中,是在字符串起始,刚刚通过图中第一个表示边界的盒子),因此若当前分支无法匹配,可以回退并尝试另一条分支。对于字符串`"103"`,遇到字符3之后,它会开始尝试匹配十六进制数字的分支,它会再次失败,因为数字后面没有h。所以它尝试匹配进制数字的分支,由于这条分支可以匹配,因此匹配器最后的会返回十进制数的匹配信息。 ![](../Images/00303.jpeg) @@ -340,41 +344,41 @@ console.log("Borobudur".replace(/[ou]/g, "a")); 如果JavaScript为replace添加一个额外参数,或提供另一个不同的方法(replaceAll),来区分替换一次匹配还是全部匹配,将会是较为明智的方案。遗憾的是,因为某些原因JavaScript依靠正则表达式的属性来区分替换行为。 -如果我们在替换字符串中使用元组,就可以体现出replace方法的真实威力。例如,假设我们有一个规模很大的字符串,包含了人的名字,每个名字占据一行,名字格式为“姓,名”。若我们想要交换姓名,并移除中间的逗号(转变成名,姓这种简单格式),我们可以使用下面的代码: +如果我们在替换字符串中使用元组,就可以体现出replace方法的真实威力。例如,假设我们有一个规模很大的字符串,包含了人的名字,每个名字占据一行,名字格式为“姓,名”。若我们想要交换姓名,并移除中间的逗号(转变成名,姓这种格式),我们可以使用下面的代码: ``` console.log( - "Hopper, Grace\nMcCarthy, John\nRitchie, Dennis" - .replace(/([\w ]+), ([\w ]+)/g, "$2 $1")); -// → Grace Hopper + "Liskov, Barbara\nMcCarthy, John\nWadler, Philip" + .replace(/(\w+), (\w+)/g, "$2 $1")); +// → Barbara Liskov // John McCarthy -// Dennis Ritchie +// Philip Wadler ``` 替换字符串中的$1和$2引用了模式中使用圆括号包裹的元组。$1会替换为第一个元组匹配的字符串,$2会替换为第二个,依次类推,直到$9为止。也可以使用$&来引用整个匹配。 第二个参数不仅可以使用字符串,还可以使用一个函数。每次匹配时,都会调用函数并以匹配元组(也可以是匹配整体)作为参数,该函数返回值为需要插入的新字符串。 -这里给出一个简单示例: +这里给出一个小示例: ``` -var s = "the cia and fbi"; -console.log(s.replace(/\b(fbi|cia)\b/g, function(str) { - return str.toUpperCase(); -})); +let s = "the cia and fbi"; +console.log(s.replace(/\b(fbi|cia)\b/g, + str => str.toUpperCase())); // → the CIA and FBI ``` 这里给出另一个值得讨论的示例: ``` -var stock = "1 lemon, 2 cabbages, and 101 eggs"; +let stock = "1 lemon, 2 cabbages, and 101 eggs"; function minusOne(match, amount, unit) { amount = Number(amount) - 1; - if (amount == 1) // only one left, remove the 's' + if (amount == 1) { // only one left, remove the 's' unit = unit.slice(0, unit.length - 1); - else if (amount == 0) + } else if (amount == 0) { amount = "no"; + } return amount + " " + unit; } console.log(stock.replace(/(\d+) (\w+)/g, minusOne)); @@ -387,7 +391,7 @@ console.log(stock.replace(/(\d+) (\w+)/g, minusOne)); ### 9.13 贪婪模式 -使用replace编写一个函数移除JavaScript代码中的所有注释并非难事。这里我们尝试一下: +使用replace编写一个函数移除JavaScript代码中的所有注释也是可能的。这里我们尝试一下: ``` function stripComments(code) { @@ -401,9 +405,9 @@ console.log(stripComments("1 /* a */+/* b */ 1")); // → 1 1 ``` -或运算符之前的部分只匹配两个斜杠字符,后面跟着任意数量的非换行字符。多行注释部分较为复杂,我们使用[^](任何非空字符集合)来匹配任意字符。我们这里无法使用句号,因为块注释可以跨行,句号无法匹配换行符。 +或运算符之前的部分匹配两个斜杠字符,后面跟着任意数量的非换行字符。多行注释部分较为复杂,我们使用[^](任何非空字符集合)来匹配任意字符。我们这里无法使用句号,因为块注释可以跨行,句号无法匹配换行符。 -但上述示例的输出明显有错。 +但最后一行的输出显然有错。 为何? @@ -432,9 +436,9 @@ console.log(stripComments("1 /* a */+/* b */ 1")); 这里给出一个示例。 ``` -var name = "harry"; -var text = "Harry is a suspicious character."; -var regexp = new RegExp("\\b(" + name + ")\\b", "gi"); +let name = "harry"; +let text = "Harry is a suspicious character."; +let regexp = new RegExp("\\b(" + name + ")\\b", "gi"); console.log(text.replace(regexp, "_$1_")); // → _Harry_ is a suspicious character. ``` @@ -443,14 +447,14 @@ console.log(text.replace(regexp, "_$1_")); 但由于我们的用户是怪异的青少年,如果用户将名字设定为“dea+hl[]rd”,将会发生什么?这将会导致正则表达式变得没有意义,无法匹配用户名。 -为了能够处理这种情况,我们可以在任何不信任的字符前添加反斜杠。在字符前添加反斜杠并不是个好主意,因为\b和\n这些字符都有特殊含义。但对所有非字母和数字字符进行转义则是安全的。 +为了能够处理这种情况,我们可以在任何有特殊含义的字符前添加反斜杠。 ``` -var name = "dea+hl[]rd"; -var text = "This dea+hl[]rd guy is super annoying."; -var escaped = name.replace(/[^\w\s]/g, "\\$&"); -var regexp = new RegExp("\\b(" + escaped + ")\\b", "gi"); -console.log(text.replace(regexp, "_$1_")); +let name = "dea+hl[]rd"; +let text = "This dea+hl[]rd guy is super annoying."; +let escaped = name.replace(/[^\w\s]/g, "\\$&"); +let regexp = new RegExp("\\b(" + escaped + ")\\b", "gi"); +console.log(text.replace(regexp, "_><_")); // → This _dea+hl[]rd_ guy is super annoying. ``` @@ -475,12 +479,12 @@ exec方法同样没提供方便的方法来指定字符串中的起始匹配位 正则表达式对象包含了一些属性。其中一个属性是source,该属性包含用于创建正则表达式的字符串。另一个属性是lastIndex,可以在极少数情况下控制下一次匹配的起始位置。 -所谓的极少数情况,指的是当正则表达式启用了全局(g),并且使用exec匹配模式的时候。此外,另一个解决方案应该是向exec传递的额外参数,但JavaScript的正则表达式接口能设计得如此合理才是怪事。 +所谓的极少数情况,指的是当正则表达式启用了全局(g)或者粘性(y),并且使用exec匹配模式的时候。此外,另一个解决方案应该是向exec传递的额外参数,但JavaScript的正则表达式接口能设计得如此合理才是怪事。 ``` -var pattern = /y/g; +let pattern = /y/g; pattern.lastIndex = 3; -var match = pattern.exec("xyzzy"); +let match = pattern.exec("xyzzy"); console.log(match.index); // → 4 console.log(pattern.lastIndex); @@ -489,10 +493,21 @@ console.log(pattern.lastIndex); 如果成功匹配模式,exec调用会自动更新lastIndex属性,来指向匹配字符串后的位置。如果无法匹配,会将lastIndex清零(就像新构建的正则表达式对象lastIndex属性为零一样)。 -若使用同一个全局正则表达式对象多次调用exec,自动更新lastIndex属性可能造成问题。正则表达式可能恰好处于前一次调用exec之后剩余字符的起始位置。 +全局和粘性选项之间的区别在于,启用粘性时,仅当匹配直接从`lastIndex`开始时,搜索才会成功,而全局搜索中,它会搜索匹配可能起始的所有位置。 ``` -var digit = /\d/g; +let global = /abc/g; +console.log(global.exec("xyz abc")); +// → ["abc"] +let sticky = /abc/y; +console.log(sticky.exec("xyz abc")); +// → null +``` + +对多个`exec`调用使用共享的正则表达式值时,这些`lastIndex`属性的自动更新可能会导致问题。 您的正则表达式可能意外地在之前的调用留下的索引处开始。 + +``` +let digit = /\d/g; console.log(digit.exec("here it is: 1")); // → ["1"] console.log(digit.exec("and now: 1")); @@ -510,14 +525,15 @@ console.log("Banana".match(/an/g)); 循环匹配 -有一种普遍模式是找出字符串中所有模式的出现位置,这种情况下,我们可以在循环中使用lastIndex和exec访问匹配的对象。 +一个常见的事情是,找出字符串中所有模式的出现位置,这种情况下,我们可以在循环中使用lastIndex和exec访问匹配的对象。 ``` -var input = "A string with 3 numbers in it... 42 and 88."; -var number = /\b(\d+)\b/g; -var match; -while (match = number.exec(input)) - console.log("Found", match[1], "at", match.index); +let input = "A string with 3 numbers in it... 42 and 88."; +let number = /\b(\d+)\b/g; +let match; +while (match = number.exec(input)) { + console.log("Found", match[0], "at", match.index); +} // → Found 3 at 14 // Found 42 at 33 // Found 88 at 40 @@ -527,10 +543,10 @@ while (match = number.exec(input)) ### 9.17 解析INI文件 -为了总结一下本章介绍的内容,我们来看一下如何调用正则表达式来解决问题。假设我们编写一个程序从因特网上获取我们敌人的信息(这里我们实际上不会编写该程序,仅仅编写读取配置文件的那部分代码,对不起,让读者失望了)。配置文件如下所示。 +为了总结一下本章介绍的内容,我们来看一下如何调用正则表达式来解决问题。假设我们编写一个程序从因特网上获取我们敌人的信息(这里我们实际上不会编写该程序,仅仅编写读取配置文件的那部分代码,对不起)。配置文件如下所示。 ``` -searchengine=http://www.google.com/search?q=$1 +searchengine=https://duckduckgo.com/?q=$1 spitefulness=9.7 ; comments are preceded by a semicolon... @@ -540,13 +556,13 @@ fullname=Larry Doe type=kindergarten bully website=http://www.geocities.com/CapeCanaveral/11451 -[gargamel] -fullname=Gargamel -type=evil sorcerer -outputdir=/home/marijn/enemies/gargamel +[davaeorn] +fullname=Davaeorn +type=evil wizard +outputdir=/home/marijn/enemies/davaeorn ``` -该配置文件格式的语法规则如下所示(实际上这是广泛使用的格式,我们通常称之为INI文件): +该配置文件格式的语法规则如下所示(它是广泛使用的格式,我们通常称之为INI文件): + 忽略空行和以分号起始的行。 @@ -563,11 +579,11 @@ outputdir=/home/marijn/enemies/gargamel ``` function parseINI(string) { // Start with an object to hold the top-level fields - var currentSection = {name: null, fields: []}; - var categories = [currentSection]; + let currentSection = {name: null, fields: []}; + let categories = [currentSection]; string.split(/\r?\n/).forEach(function(line) { - var match; + let match; if (/^\s*(;.*)?$/.test(line)) { return; } else if (match = line.match(/^\[(.*)\]$/)) {