GitHub 链接:javascript-the-definitive-guide
上一章链接:Set,Map 和 Typed Array
正则表达式(Pattern Matching with Regular Expressions)
正则表达式是用来表示文字符串 pattern 的对象。JavaScript 中的 RegExp 类表示了正则表达式,String 和 RegExp 都有许多调用正则表达式的方法。
定义正则表达式(Defining Regular Expressions)
在 JavaScript 中,正则表达式是通过 RegExp 对象表示的。RegExp 对象可以通过 RegExp() 构造函数来创建,或者直接使用正则字面量句法。正如字符串字面量是在一对引号中定义的,正则表达式字面量是在一对斜杠(slash /)中定义的:
let pattern1 = /a/;
let pattern2 = new RegExp('a'); // 效果同上
上面的代码创建了一个新的 RegExp 对象,然后将其赋值给了一个变量。
正则表达式会包含一系列的字符,大多是是英文字符,其表示了我们希望匹配的字符。就比如说 /a/ 会匹配所有包含子字符串 'a' 的字符串。除了需要匹配的字符串外,其中还包了特殊的元字符,用于表示匹配时的特征。在一对斜杠之后(或构造函数的第二个 argument)还可以包括一个或多个标志(flags)。
字面量字符(LITERAL CHARACTERS)
在正则表达式中,每个英文字母和数字的字面量都能匹配他们本身,JavaScript 也支持某些非字母或者数字的字面量,它们通过在前面加上一个反斜杠(backslash \)来表示。具体的支持如下:
- 字符 --- 匹配
- 字母和数字 --- 其本身
- \0 --- 空字符,即空格(\u0000)
- \t --- Tab(\u0009)
- \n --- 换行符(\u000A)
- \v --- 垂直 Tab(\u000B)
- \f --- 换页符(\u000C)
- \r --- 回车符(\u000D)
- \xnn --- 两位的十六进制数 nn 表示的字符(如 \x0A 等同于 \n,因为 \n 是 \u000A)
- \uxxxx --- 四位的十六进制数 nn 表示的字符(如 \u000A 等同于 \n)
- \u{n} --- 十六进制数表示的 UTF-16 代码单元(只能在有 u flag 的正则表达式中使用)
- \cX --- 匹配字符串中的一个控制符
许多标点符号在正则中有特殊的含义,他们是:
^ $ . * + ? = ! : | \ / ( ) [ ] { }
他们的含义会在等一下被介绍,其中某些只有在指定的上下文中有着特殊的含义,在某些情况又被当作成普通的字面量。总而言之,如果想要在正则匹配中加入这些符号,我们须在其前面加上反斜杠 \ 作为前缀。 虽然某些符号不使用反斜杠 \ 也能正常进行匹配,但是加上了反斜杠 \ 也不会对其造成影响。不过数字和字母则不同,加上反斜杠 \ 可能会带来不同的结果。如果想要匹配一个反斜杠,则需要用 \ 表示。
字符类(CHARACTER CLASSES)
单一的字符字面量放在一对中括号的情况下可以被组合成字符类,字符类会匹配其中包括的每一个字符。所以正则表达式 /[abc]/ 会匹配字母 a,b,c 中的任意一个。
我们也可以反方向进行定义 /[^abc]/,这样在中括号中的第一位加上一个插入符(^)可以使该正则表达式匹配所有非 a,b,c 的字符。
字符类也可以通过加上连字符(-)来表示一个 range。比如 /[a-z]/ 会匹配所有小写字母;/[a-zA-Z0-9]/ 会匹配所有字母或者数字。
因为许多类似的字符类会被经常使用,所以它们也用特殊的句法来表示:
- 字符 --- 匹配
- [...] --- 任何一个中括号内的字符
- [^...] --- 任何一个非中括号内的字符
- .(小数点) --- 匹配除去换行符以外的任何字符(在拥有 s flag 时,他也会匹配换行符)
- \w --- 任何 ASCII 单字字符,等同于 /[a-zA-Z0-9_]/(包含下划线 _ )
- \W --- 任何非 ASCII 单字字符,等同于 /[^a-zA-Z0-9_]/
- \s --- 任何 unicode 的空白字符,包括空格、Tab、换页符、换行符
- \S --- 任何非 unicode 的空白字符
- \d --- 任何 ASCII 数字,等同于 /[0-9]/
- \D --- 任何非 ASCII 数字的字符,等同于 /[^0-9]/
- [\b] --- 退格 backspace 的字面量
重复(REPETITION)
基于前面的句法,我们现在可以用 /\d\d/ 来描述一个两位数,或者 /\d\d\d\d/ 来描述一个四位数。但其实我们还有更简便的方法,比如,对于一个可能包含任何长度的数字,或者数字和字符的结合来说时,正则表达式其实拥有特有的句法来表示重复的次数。这些用于表示重复次数的字符会一直紧跟在它们需要描述的 patter 之后:
- 字符 --- 匹配
- {n, m} --- 匹配其前面的字符至少 n 次,但不大于 m 次
- {n,} --- 匹配其前面的字符至少 n 次
- {n} --- 匹配其前面的字符 n 次
- ? --- 匹配其前面的字符 0 次 或 1 次,即 {0, 1}
- + --- 匹配其前面的字符至少 1 次,即 {1, }
- * --- 匹配其前面的字符至少 0 次,即 {0, }
let p1 = /\d{2,4}/; // 匹配两位数到四位数
let p2 = /\w{3}\d?/; // 匹配一个长度为三的单词加上一个可选数字
let p3 = /\s+hello\s+/; // 匹配字符拥有开头和结尾空白的 'hello'
let p4 = /[^(]*/; // 匹配任意数量的非左括号字符
需要注意的时,任何表示重复次数的字符只会影响其前一个字符或字符类,若需要用重复数字来影响更多的字符,则需要使用括号将它们包裹。这会在接下来被介绍。
在使用 * 和 ? 要注意它们会从零开始匹配,所以即使没有那个需要匹配的字符,它们也会成功匹配。比如 /a*/ 也能匹配到 'bbb'。
非贪婪重复(NON-GREEDY REPETITION)
在使用前面的重复匹配模式中,它们会尽可能多次的匹配。即使已经有匹配了,它们也会接着进行匹配,我们称之为贪婪(greedy)重复。
定义一种非贪婪重复其实也十分简单,只需要在原本的重复字符后加上一个问号 ? 即可。比如 ??, +?, *?, {1,5}? 等等。 比如在对 'aaa' 使用 /a+/ 匹配时,三个字符都会被匹配;但是 /a+?/ 则只会匹配 'aaa' 中的第一个字母 a。
选项,组合和引用(ALTERNATION, GROUPING, AND REFERENCES)
正则表达式的语法中也包含了用来进行选项匹配,
在进行选项匹配时,| 字符被用于分开选项。比如 /ab|cd|ef/ 可以匹配字符串 'ab' 或 'cd' 或 'ef';/\d{3}|[a-z]{4}/ 可以匹配三个数字或者四个小写字母。需要注意的时匹配是通过从左往右的顺序进行的,只要匹配到就会返回匹配值,不论右边是否有更好的匹配。比如用 /a|ab/ 匹配 'ab' 时,匹配到的会是 'a'。
在正则表达式中,括号有许多的目的,其中之一便是将多个分开的字符或字符类整合到一起作为一段子表达式,然后可以被 |, ?, + 等符号当成一个元素处理。比如 /java(script)?/ 会匹配 'java',以及之后可选的 'script'; /(ab|cd)+|ef/ 会匹配 'ef' 或者 至少重复一次的任意字符串 'ab' 或 'cd'。
括号的另一个目的则是在 pattern 中定义 subpatterns。在正则表达式匹配成功时,我们可以从中提取一段字符串。比如在匹配一段字母加一段上数字的时候,我们也想同时提取其中的数组,我们可以使用 /[a-z]+(\d+)/,用括号来包裹数字部分,这样在匹配时,数字部分也会被匹配。
在使用括号定义完子表达式后,我们还可以在同一正则表达式中重新引用那一段子表达式。这可以使用反斜杠
加上一个数字来表示。数字用于表示括号中子表达式的位置。比如 \1 会指向第一个括号中的子表达式,\2 会指向第二个。因为子表达式也可以内嵌在子表达试中,所以对于顺序定义是其左括号出现的顺序。
let r = /([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/;
比如上面的例子中,子表达式 ([Ss]cript) 就出现在了第二位,我们可以用 \2 来引用它。但其实在引用时,他并不是以用其 pattern,而是引用匹配到的文本。所以这个引用可以被用于加强对于两段分开的字符串的限制:
let p = /['"][^'"]*['"]/;
比如上面的例子中,会匹配单引号或双引号中的一段字符,但是它并不要求起始的引号和结尾的引号是同样的。如果想要引号相同,则可以这么更改:
let p = /(['"])[^'"]*\1/;
其中的 \1 会匹配和第一对括号中相同的文本,它规定了结束的引号必须和起始的引号相同,否则就不匹配。(注意,无法在字符类中引用子表达式,即无法在中括号 [...] 中使用 \1)
我们也可以在不创建数字引用的情况下将正则表达式中的元素组合。要这么做的话,我们需要使用 (?: ... ) 来代替 ( ... ):
let p = /([Jj]ava(?:[Ss]cript)?)\sis\s(fun\w*)/;
在这个例子中,子表达式 (?:[Ss]cript) 可以用来组合其中的字符和字符类,所以之后的 ? 字符会对这个子表达式生效;但是它们不会产生数字引用,所以使用 \2 时会直接指向 (fun\w*)。
选项,组合,引用的字符小结:
- 字符 --- 含义
- | --- 选项:用于分割选项,只需要匹配其左边或者右边的子表达式即可
- ( ... ) --- 组合:将字符和字符类归类组合到一起,是它们可以以用被 *, +, ?, | 等字符操控,并且创建引用
- (?: ... ) --- 无引用组合:将字符和字符类归类组合到一起,是它们可以以用被 *, +, ?, | 等字符操控
- \n --- 引用:其中 n 为数字,引用被括号分割的子表达式,子表达式的顺序是根据其左括号出现的顺序而定的,无引用括号不会创建引用
在 ES2018 中,对于正则表达式有了一个新的特性,使其可以使用名为 named capture groups 来给每一个括号中的子表达式命名,并且方便之后的引用:
let p = /(?<city>\w+) (?<state>[A-Z]{2})/; // 对每一个子表达式用 <> 的形式来命名,增加可读性
let p2 = /(?<quote>['"])[^'"]*\k<quote>/; // 以标签名引用子表达式
指定匹配位置(SPECIFYING MATCH POSITION)
在前面的介绍中,许多正则表达式的元素都只能能对字符串中的一个字符进行匹配。但其实这里还有许多字符使我们可以对字符的位置进行匹配。比如说 '\b' 字符,它对一个 ASCII 单词进行边界(boundary)的匹配;或者 ^ 表示了 pattern 要在字符串的开始;或者 $ 表示了 pattern 要在字符串的结束。
比如说要去匹配单行的单词 'JavaScript' 我们可以使用 /^JavaScript$/。
指定匹配位置的字符有以下这些:
- 字符 --- 含义
- ^ --- 匹配字符串的开始(在有 m flag 时,匹配一行的开始)
- $ --- 匹配字符串的结束(在有 m flag 时,匹配一行的结束)
- \b --- 匹配一个单词的边界,一个 \w 字符的开头或者结尾为一个 \W 字符
- \B --- 与 \b 相反
- x(?= y) --- 向前断言:x 在被 y 跟随时,匹配 x
- x(?! y) --- 向前否定断言:x 在不被 y 跟随时,匹配 x
- (?<= y)x --- 向后断言:x 跟随在 y 之后时,返回 x
- (?<! y)x --- 向后否定断言:x 不跟随在 y 之后时,返回 x
标志(FLAGS)
每一个正则表达式都可以拥有一个或多个的于其关联的 flag 用于改变其匹配方式。在 JavaScript 中,一共有六个 flags,每一个都由单一的字母表示。flags 会在正则表达式字面量的第二个反斜杠之后定义,或者作为构造函数的第二个 argument 传入。支持的 flags 如下:
g: g flag 表示正则表达式是全局的(global)。这意味着他会去寻找所有的匹配值而非在找到第一个后停下。他并不会改变匹配的方法。
i: i flag 指定了匹配时不用区分大小写。
m: m flag 指定了匹配会在多行(multiline) 模式下进行。这意味着匹配会对每一行进行,所以类似 ^ 和 $ 的字符不仅会匹配字符串的开始和结束,还会匹配行的开始和结束。
s: s flag 也和 m flag 一样,经常被使用于多行匹配的情况。通常情况下,'.' 可以匹配除换行符以外的所有字符;在拥有 s flag 时,'.' 也可以匹配换行符。
u: u flag 表示 Unicode,他是正则表达式可以对 Unicode 的 codepoints 进行匹配而非 16 位值的字符。(通常情况下可以一直对正则表达式开启这个 flag,否则正则表达式在对于类似汉字或者 emoji 时无法正确匹配)
y: y flage 表示正则表达式是执行粘性(sticky)匹配的。他会匹配到字符串的开头或者从其上一个匹配值开始的字符。对于只要寻找一个匹配值的正则表达式,他等同于使用了 ^ 前缀;对于需要找更多匹配值时,他则会比较有用。
这些 flag 可以以任何顺序被组合然后影响同一个正则表达式。
使用正则表达式的字符串方法(String Methods for Pattern Matching)
到目前为止,我们已经知道如何定义正则表达式和它的语法了,但是并没有节是如何在 JavaScript 中使用它们。现在我们会讲一下字符串使用 RegExp 时的 API。
search() 方法
字符串一共有四个和正则表达式有关的方法,其中最简单的便是 search()。它会接受一个正则表达式作为 argument,然后返回找到的匹配的位置,或者在找不到时返回 -1:
"JavaScript".search(/script/ui) // 4
"Java".search(/script/ui) // -1
如果传入的参数不是正则表达式的话,他会先将其传入正则表达式的构造函数然后再进行匹配;search() 也不支持全局搜索,所以会忽略 g flag。
replace() 方法
replace() 方法会进行先找到,后替替换的操作。他会接受一个正则表达式作为第一个 argument,用于替换的字符串作为第二个 argument。如果正则表达式拥有 g flag,则它会替代所有匹配的字符串,若没有 g flag,则只会替代第一个匹配的字符串。在第一个 argument 为字符串的情况下,他不会将其转换成正则表达式,而是直接搜索那个字符串字面量。
'java java'.replace(/java/gu, "JavaScript"); // "JavaScript JavaScript",传入 g flag,全部替换
'java java'.replace(/java/u, "JavaScript"); // "JavaScript java",不传入 g flag,替换第一个
'java java'.replace('java', "JavaScript"); // "JavaScript java",也可以直接传入字符串
事实上,replace() 还拥有更为强大的功能。回想一下,我们前面通过括号来定义了子表达式,并且可以通过数字来对他们进行从左到右的引用。如果在用于替代的字符串中出现了 '$n' (n 为数字)的话,它会根据匹配到的字符串来进行替代:
let quote = /"([^"]*)"/g; // 匹配所有双引号中的内容
'He said "hello"'.replace(quote, '$1!!!') // "He said hello!!!"
let quote = /"(?<quotedText>[^"]*)"/g; // 使用了 name capture group 时也可以
'He said "hello"'.replace(quote, '$<quotedText>!!!') // // "He said hello!!!"
我们甚至可以传入一个函数作为用于替代的字符串,这个函数会被调用然后其返回值会作为用于替代的字符串:
"11 times 22 is 242".replace(/\d+/gu, n => (+n).toString(2));
// "1011 times 10110 is 11110010" 这个函数将每一个匹配到的数字转换成了 2 进制数
match() 方法
match() 方法也是最常见的方法之一了,他会接受一个正则表达式作为 argument(若不为正则表达式则传入正则表达式构造函数进行构造),然后返回一个获得的匹配值的数组,或者在没有找到时返回 null。在正则表达式有 g flag 时,返回所有匹配到的结果:
"11 times 22 is 242".match(/\d+/g); // ["11", "22", "242"]
但如果正则表达式没有 g flag 的话,它会找到第一个匹配值,然后返回一个数组。但是这个数组中的元素则和上面的不同,其中第一个元素是找到的字符串,剩余的元素则是其正则表达式括号中的子表达式的匹配值。这个数组也拥有一些出额外的属性:
let urlPatter = /(\w+):\/\/([\w.]+)\/(\S*)/;
let url = 'the url is https://www.abc.com/about/me';
let match = url.match(url);
match // ["https://www.abc.com/about", "https", "www.abc.com", "about/me"]
match.input // 返回输入的文本,"the url is https://wwasdom/about/asd"
match.index // 返回找到匹配时的字符串索引, 11
match.group // undefined
// match.group 在正则表达式包含 named capture groups 时返回一个group 对象,
// group 对象包含了 name groups name 作为属性名;匹配的文本作为值。
// 若没有使用 named capture groups,返回 undefined
matchAll() 方法
matchAll() 方法是在 ES2020 新增的方法,它接受一个拥有 g flag 的正则表达式,然后返回一个可以 yields 每一个对 match() 使用非全局正则表达式的返回值的 iterator:
const wordsPatter = /\b\w+\b/gu; //匹配每一个单词
let words = 'how are you';
for (let word of words.matchAll(wordsPatter)) {
console.log(word.index + ': ' + word[0]);
}
// 0: how
// 4: are
// 8: you
split() 方法
split() 方法其实也可以接受一个正则表达式:
'1,2,3,4,5'.split(','); // ['1','2','3','4','5']
'1,2,3,4,5'.split(/,/); // ['1','2','3','4','5']
'1,2,3,4,5'.split(/\d/); // ["", ",", ",", ",", ",", ""]
RegExp 类(The RegExp Class)
RegExp 的构造函数
首先要介绍的就是 RegExp 类的构造函数了,他可以接受一到两个字符串作为 arguments,然后构建一个新的 RegExp 对象。第一个 argument 是一个表达正则表达式本体的字符串,即出现在反斜杠之间的部分,第二个 argument 是表示 flags 的字符串,即出现在反斜杠之后的部分:
let p = new RegExp('a','g'); // 效果等同于 let p = /a/g;
在正则表达式是动态的时候,使用构造函数就很有必要了。在传入第一个 argument 时,我们其实也可以直接传入一个正则表达式,这样的目的主要是复制这个正则表达式然后改变它的 flag:
let p1 = /JavaScript/g;
let p2 = new RegExp(p1, "i"); // p2 = /JavaScript/i
RegExp 的属性
RegExp 对象上拥有以下属性:
- source: 只读属性,表示了正则表达式的出现在反引号之间的源字符串
- flags: 只读属性,表示了正则表达式的 flag
- global: 只读布尔值属性,在拥有 g flag 时为 true
- ignoreCase: 只读布尔值属性,在拥有 i flag 时为 true
- multiline: 只读布尔值属性,在拥有 m flag 时为 true
- dotAll: 只读布尔值属性,在拥有 s flag 时为 true
- unicode: 只读布尔值属性,在拥有 u flag 时为 true
- sticky: 只读布尔值属性,在拥有 y flag 时为 true
- lastIndex: 可读写属性,在拥有 g 或 y flags 的正则表达式上,这个属性的值表示了下一次搜索开始的索引,会在接下来的方法中被用到
RegExp 的方法
test() 方法
RegExp 类中的 test() 方法是使用正则表达式最简单的方法之一。它接受一个字符串作为 argument,然后在匹配时返回 true,在不匹配时返回 false。
test() 的运作方式很简单,他会直接调用下面的 exec() 方法然后再其返回一个非 null 值的时候返回 true。
exec() 方法
RegExp 类中的 test() 方法则是最常被使用的方法。它会接受一个字符串作为 argument,然后将其和正则表达式比较,如果没有匹配值,返回 null,如果有匹配值,它会返回一个等同于调用 match() 进行非全局搜索时的返回的数组。索引为 0 的元素是其匹配值,剩下的元素则是对子表达式的匹配。
不像字符串的 match() 方法,exec() 不论有没有 g flag,都会返回一样类型的数组。所以它也只会返回其中第一个匹配值。在对拥有 g 或者 y flag 的正则表达式进行调用时,它会根据 lastIndex 属性作为其开始查找的位置。
对于新创建的 RegExp 对象,其 lastIndex 属性是 0,每当对其调用 exec() 并且成功的找到一个值时,lastIndex 的值会被更新为匹配值的下一个字符的索引。这可以让我们在循环中多次调用 exec() 然后查找所有匹配值。(在 ES2020 后,使用 字符串的 matchAll() 方法会是一个更好的选择
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!