在编程世界中,处理文本数据是一项频繁而重要的任务。无论是验证用户输入的邮箱格式,还是从一大段文字中提取特定信息, 我们都需要强大的文本处理工具。正则表达式就是这样一个工具——它虽然语法看起来有些神秘,但却是处理字符串的利器。
正则表达式本质上是一种用来描述字符串模式的特殊语言。在JavaScript以及许多其他编程语言中, 正则表达式都扮演着重要角色。虽然初学者可能会觉得正则表达式的语法令人困惑,但一旦掌握了基本概念,你就会发现它在文本处理方面的强大能力。

在JavaScript中,我们有两种方式来创建正则表达式。第一种是使用RegExp构造函数,第二种是使用字面量语法,即将模式放在两个斜杠之间。
|let pattern1 = new RegExp("北京"); let pattern2 = /北京/;
这两种方式创建的正则表达式都表示相同的模式:匹配字符"北京"。当我们使用构造函数方式时,模式作为普通字符串传入,因此需要遵循字符串的转义规则。而使用字面量语法时,反斜杠的处理方式略有不同。
在字面量语法中,由于正斜杠用来标记模式的开始和结束,如果我们希望在模式中包含正斜杠字符,就需要用反斜杠进行转义。另外,一些具有特殊含义的字符,比如问号和加号,如果要表示其字面意思,也需要用反斜杠转义。
|let websitePattern = /https:\/\/www\.example\.com/;
正则表达式对象提供了多种方法来处理字符串。其中最简单的是test方法,它接受一个字符串参数,返回布尔值来表示字符串是否包含匹配的模式。
|console.log(/苹果/.test("我喜欢吃苹果")); console.log(/苹果/.test("我喜欢吃香蕉"));
|true false
仅由普通字符组成的正则表达式会匹配字符串中任何位置出现的相同字符序列。 只要目标字符串中包含这个序列,test方法就会返回true,而不要求这个序列必须出现在字符串的开头。
正则表达式之所以强大,是因为它能够灵活地描述各种复杂的字符串模式。比如说,如果我们希望检测一个字符串中是否包含数字,可以借助方括号来实现。方括号在正则表达式中用来指定一个字符集合,意思是只要目标字符串中出现了集合里的任意一个字符,就算匹配成功。举例来说,写成[0123456789],就表示“0到9之间的任意一个数字”,这样我们就可以轻松判断字符串中是否包含数字字符。
|console.log(/[0123456789]/.test("房间号是208"));
|true
在正则表达式的方括号中,我们可以使用连字符“-”来指定一个范围,这样就不需要把所有可能的字符都一一列出。比如,数字字符在Unicode编码表中是连续排列的,从“0”到“9”依次递增,因此我们可以直接写成[0-9],这表示匹配任意一个数字字符。这样不仅写法更简洁,也更容易阅读和维护。
|console.log(/[0-9]/.test("房间号是208"));
|true
JavaScript提供了一些常用字符组的简写形式。数字字符可以用\d表示,这等同于[0-9]。除此之外,还有其他一些有用的简写:
利用这些简写,我们可以构建一个匹配日期时间格式的表达式:
|let datePattern = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/; console.log(datePattern.test("今天是2024-03-15 14:30"));
|true
虽然这个表达式能够工作,但看起来有些冗长。在后面的内容中,我们会学习如何让它更加简洁。
如果想要匹配除了指定字符以外的任意字符,可以在方括号开头使用脱字符(^):
|let notBinary = /[^01]/; console.log(notBinary.test("1100101")); console.log(notBinary.test("1100201"));
|false true
前面我们已经学会了如何用正则表达式匹配单个字符,比如一个数字字符\d。但在实际应用中,我们经常需要匹配由多个数字字符组成的完整数字,
比如身份证号、电话号码等。这时,仅仅用一个\d还不够,因为它只能匹配单个数字。为了解决这个问题,正则表达式提供了重复匹配的机制。
加号(+)就是其中最常用的一个重复符号。它的作用是让它前面的元素可以连续出现一次或多次,也就是说,只要目标字符串中有连续的数字字符,不管有几个,都能被\d+这个模式完整地匹配到。
|console.log(/'\d+'/.test("'12345'")); console.log(/'\d+'/.test("''"));
|true false
星号(*)是正则表达式中的重复符号之一。它的作用是让它前面的元素可以重复出现零次或多次。也就是说,被星号修饰的部分可以完全不出现(出现零次),也可以出现一次、两次,甚至更多次。例如,\d* 可以匹配零个、一个或多个数字字符。这意味着即使目标字符串中没有对应的字符,整个模式依然可以匹配成功。下面通过例子来说明:
|console.log(/'\d*'/.test("'12345'")); console.log(/'\d*'/.test("''"));
|true true
问号(?)在正则表达式中用于表示它前面的内容是可选的,也就是说,这部分内容可以出现一次,也可以完全不出现。举个例子,如果我们想同时匹配英式和美式拼写的单词,比如"color"和"colour",就可以在"u"字母后面加上问号,这样正则表达式既能匹配带"u"的"colour",也能匹配不带"u"的"color"。
|let colorPattern = /colou?r/; console.log(colorPattern.test("color"));
|true
|console.log(colorPattern.test("colour"));
|true
如果你想让某个模式重复出现特定的次数,可以在它后面加上一对花括号,并在花括号中写上具体的数字。例如,写成4时,表示前面的内容必须连续出现四次,不能多也不能少。如果写成{2,5},则表示前面的内容可以连续出现两次、三次、四次或五次,任何在这个范围内的次数都可以被匹配。
|let phonePattern = /\d{3}-\d{4}-\d{4}/; console.log(phonePattern.test("138-1234-5678"));
|true
省略花括号中逗号后的数字表示"至少重复指定次数"。{3, }表示至少重复3次。
如果我们希望让一组字符整体重复出现,而不是只让单个字符重复,就需要用圆括号把这部分内容括起来。这样,括号中的内容会被正则表达式当作一个整体处理,重复操作符就会作用于整个括号内的部分,而不是括号前的单个字符。例如,如果你想让“呵”字可以连续出现多次,并且希望“哈”字和“呵”字的组合也能整体重复,这时就需要用括号把“呵”字包起来,然后在括号后面加上重复符号。这样,正则表达式会把括号内的内容当作一个整体来判断它出现的次数。
|let laughPattern = /哈+(呵+)+/; console.log(laughPattern.test("哈哈呵呵呵呵呵"));
|true
在这个例子中,第一个加号作用于"哈"字,第二个加号作用于"呵"字,而第三个加号作用于整个"(呵+)"分组,表示这样的笑声可以重复多次。
test方法只能判断字符串中是否存在匹配项,但有时候我们不仅仅想知道有没有匹配,还希望获得匹配的内容本身以及它在字符串中的具体位置。这时可以使用正则表达式的exec方法。exec方法会在目标字符串中查找匹配项,并返回一个包含详细信息的数组,比如匹配到的文本和它在原字符串中的起始索引。如果没有找到匹配,exec方法会返回null。
|let match = /\d+/.exec("今年是2024年"); console.log(match);
|["2024"]
|console.log(match.index);
|3
通过exec方法返回的对象中,index属性表示匹配内容在原始字符串中的起始下标。除了exec方法之外,字符串自身还提供了match方法,也可以用来查找并获取匹配的内容,二者在用法和返回结果上有许多相似之处。
|console.log("今年是2024年".match(/\d+/));
|["2024"]
如果在正则表达式中使用圆括号将部分内容分组,那么每当匹配成功时,除了整个匹配的文本会作为结果数组的第一个元素返回之外,括号中每个分组实际匹配到的内容也会依次作为数组的后续元素出现。这样,我们不仅能获得完整的匹配,还能直接提取出括号内指定的子内容,非常适合用来捕获和提取字符串中的特定片段。
|let quotePattern = /'([^']*)'/; console.log(quotePattern.exec("她说'你好'"));
|["'你好'", "你好"]
整个匹配总是数组的第一个元素,然后依次是各个分组的匹配结果。这个特性在提取字符串的特定部分时非常有用。
JavaScript内置了Date类来处理日期和时间。创建当前时间的Date对象很简单:
|console.log(new Date());
|2024-03-15T14:30:00.000Z
也可以创建特定时间的Date对象:
|console.log(new Date(2024, 2, 15)); // 注意:月份从0开始
|2024-03-15T00:00:00.000Z
在JavaScript中,创建日期对象时需要注意,月份的编号是从0开始的,也就是说1月对应的数字是0,3月则是2,而日期的天数则是从1开始计数。这种设计很容易让人混淆,尤其是在手动解析字符串并传递参数给Date构造函数时,务必要小心,避免出现月份偏差。
利用正则表达式的分组特性,我们可以方便地从类似“2024-03-15”这样的字符串中提取出年份、月份和日期。通过exec方法匹配后,分组会分别捕获到年、月、日的具体数值,然后我们可以将这些数值传递给Date构造函数,注意要将月份减去1,才能得到正确的日期对象。
|function getDate(dateString) { let [, year, month, day] = /(\d{4})-(\d{1,2})-(\d{1,2})/.exec(dateString); return new Date(year, month - 1, day); } console.log(getDate("2024-03-15"));
|2024-03-15T00:00:00.000Z
这里使用了数组解构赋值,下划线用来忽略完整匹配的结果。
在实际应用中,很多时候我们希望正则表达式只在特定位置进行匹配,而不是在字符串的任意部分都能找到结果。为此,正则表达式提供了边界标记, 可以精确地控制匹配发生的位置。比如,脱字符(^)用来指定匹配必须发生在字符串的开头,而美元符号则要求匹配出现在字符串的结尾。通过这两个符号的组合, 可以判断一个字符串是否完全符合某种模式,而不是仅仅包含某个片段。例如,^\d+$ 这个模式就要求整个字符串只能由数字组成,否则匹配不会成功。
|console.log(/^\d+$/.test("12345"));
|true
|console.log(/^\d+$/.test("abc12345"));
|false
在正则表达式中,\b 代表单词边界。它的作用是让匹配只发生在单词的起始或结束位置,也就是说,\b不会匹配具体的字符,而是匹配单词和非单词字符之间的“缝隙”。这样可以保证我们查找的内容不会被其他字母、数字或下划线连接在一起。例如,使用 /\b猫\b/ 这个模式时,只有当“猫”是一个独立的词时才会匹配成功,而不会匹配“猫咪”这样的词,因为“猫”后面紧跟着“咪”并不在单词边界上。
|console.log(/\b猫\b/.test("我喜欢猫咪"));
|false
|console.log(/\b猫\b/.test("我喜欢 猫 和狗"));
|true
在正则表达式中,管道符号(|)用于表示“或”的关系。它允许我们在同一个模式中指定多个可选项,正则会尝试匹配其中任意一个即可。例如,/苹果|香蕉|橘子/ 这个模式能够匹配“苹果”、“香蕉”或“橘子”这三个词中的任意一个。当字符串中出现这些词中的任何一个时,正则表达式的匹配就会成功。
|let fruitPattern = /苹果|香蕉|橘子/; console.log(fruitPattern.test("我买了苹果"));
|true
在正则表达式中,圆括号不仅用于分组和捕获,还可以用来明确指定管道符(|)的作用范围。没有括号时,管道符会作用于它两边的整个表达式;而加上括号后,只有括号内的内容才会被视为可选项。例如,/\d+ (猫|狗|鸟)/ 这个模式中,括号将“猫”、“狗”、“鸟”分组,表示数字后面可以跟“猫”或“狗”或“鸟”中的任意一个。如果没有括号,管道符会影响到整个表达式,导致匹配结果与预期不同。通过使用括号,可以精确控制“或”关系的范围,使正则表达式的匹配逻辑更加清晰和灵活。
|let animalPattern = /\d+ (猫|狗|鸟)/; console.log(animalPattern.test("我有3 猫"));
|true
在实际开发中,replace方法经常与正则表达式结合使用,用于查找并替换字符串中符合特定模式的内容。最常见的场景是将匹配到的文本替换为新的内容,这样可以方便地对字符串进行批量修改或格式化。
|console.log("苹果苹果香蕉".replace(/苹果/, "橘子"));
|橘子苹果香蕉
当我们希望将字符串中所有符合某个模式的内容都进行替换时,必须在正则表达式后面加上全局标志g。这样,replace方法才会查找并替换所有匹配的部分,而不仅仅是第一个匹配项。
|console.log("苹果苹果香蕉".replace(/苹果/g, "橘子"));
|橘子橘子香蕉
在使用正则表达式进行字符串替换时,我们不仅可以简单地将匹配到的内容替换为指定的新文本,还能够通过捕获分组,将匹配结果的某些部分灵活地重新组合到替换结果中。例如,如果我们有一串以“名, 姓”格式排列的姓名,现在希望将其顺序调整为“姓 名”,就可以利用正则表达式的分组功能,在替换字符串中通过特殊的占位符(如2)来引用分组内容,从而实现姓名顺序的交换。
|let names = "张三, 李四\n王五, 赵六"; console.log(names.replace(/(\w+), (\w+)/g, "$2 $1"));
|李四 张三 赵六 王五
2分别代表第一个和第二个捕获分组的内容。
除了可以直接传入字符串作为替换内容,replace方法还支持将一个函数作为第二个参数。 这样做时,每当正则表达式匹配到内容时,replace方法都会调用这个函数,并将匹配结果及分组内容作为参数传递给它。 函数的返回值会作为本次匹配的替换结果。通过这种方式,我们可以根据每次匹配到的内容动态生成替换文本,实现更灵活、更复杂的替换逻辑。
|let text = "今天买了5个苹果和3个香蕉"; function reduceOne(match, number, fruit) { number = parseInt(number) - 1; if (number === 0) { return "没有" + fruit; } return number + "个" + fruit; } console.log(text.
|今天买了4个苹果和2个香蕉
在正则表达式中,像*、+、{n,}这样的重复操作符在默认情况下具有“贪婪”特性。所谓贪婪,就是它们会尽可能多地匹配字符,直到整个表达式能够满足为止。这种行为有时会导致匹配范围超出我们的预期,比如在处理多段注释或标签时,可能会把中间的内容也一并包含进去,从而产生意外的结果。
|function removeComments(code) { return code.replace(/\/\*.*\*\//g, ""); } console.log(removeComments("a = 1; /* 注释1 */ b = 2; /* 注释2 */ c = 3;"));
|a = 1; c = 3;
在上面的例子中,出现的结果并不理想。原因在于正则表达式中的重复操作符*默认是贪婪的,它会尽可能多地匹配字符,导致把第一个注释和第二个注释之间的所有内容都包含进了匹配结果,从而把中间的代码也一并删除了。如果我们希望每次只匹配一对注释,而不是把中间所有内容都吞掉,可以在重复操作符后面加上一个问号?,这样就能让它变成非贪婪模式。非贪婪模式下,匹配会在遇到第一个满足条件的位置就停止,从而只删除每一对注释本身,不会影响注释之间的其他代码。
|function removeComments(code) { return code.replace(/\/\*.*?\*\//g, ""); } console.log(removeComments("a = 1; /* 注释1 */ b = 2; /* 注释2 */ c = 3;"));
|a = 1; b = 2; c = 3;
在实际开发过程中,有些情况下我们并不能提前确定正则表达式的具体内容,而是需要根据用户输入或程序运行时的变量来动态生成匹配模式。此时,无法直接使用正则表达式字面量语法,因为字面量要求模式在代码编写阶段就已经确定。为了解决这个问题,我们可以借助RegExp构造函数,它允许我们将字符串形式的模式和可选的修饰符作为参数,在代码运行时动态创建出所需的正则表达式对象。
|let userName = "小明"; let text = "小明是个好学生,小明很努力"; let pattern = new RegExp("\\b(" + userName + ")\\b", "g"); console.log(text.replace(pattern, "【$1】"));
|【小明】是个好学生,【小明】很努力
注意我们需要使用双反斜杠,因为这是在普通字符串中写正则表达式。
如果用户名中包含特殊字符,需要先进行转义:
|let userName = "user+123"; let escaped = userName.replace(/[\\[.+*?(){|^$]/g, "\\$&"); let pattern = new RegExp("\\b" + escaped + "\\b", "g");
除了replace和test方法之外,字符串对象还拥有一个search方法。这个方法用于在字符串中查找与指定正则表达式匹配的内容,并返回第一个匹配项的起始位置。如果字符串中存在匹配项,search会返回其索引值;如果没有找到任何匹配,则会返回-1。这样我们可以很方便地判断某个模式是否出现在字符串中,以及它首次出现的位置。
|console.log(" 你好世界".search(/\S/));
|2
|console.log(" ".search(/\S/));
|-1
当我们创建一个带有全局(g)修饰符的正则表达式对象时,它会拥有一个名为lastIndex的属性。这个属性用于记录下一次匹配时从源字符串的哪个位置开始查找。当你多次调用exec方法或者在循环中查找所有匹配项时,lastIndex会自动更新为上一次匹配结束的位置。你也可以手动设置lastIndex的值,从而控制下一次匹配的起点。这种机制让我们能够灵活地遍历字符串中的所有匹配内容,或者从指定位置开始查找,而不仅仅是从头开始。
|let pattern = /\d/g; pattern.lastIndex = 3; let match = pattern.exec("ab123cd"); console.log(match.index);
|3
通过利用 lastIndex 属性,我们可以在字符串中逐步查找每一个匹配项,实现对所有匹配内容的遍历。每次调用 exec 方法时,正则表达式会从 lastIndex 指定的位置开始查找下一个匹配,并在找到后自动更新 lastIndex 的值。这样就能够连续获取字符串中所有符合条件的部分,而不必手动管理查找位置。
|let input = "文件中有3个数字:42和88"; let numberPattern = /\d+/g; let match; while (match = numberPattern.exec(input)) { console.log("找到数字", match[0], "位置在", match.index); }
|找到数字 3 位置在 3 找到数字 42 位置在 7 找到数字 88 位置在 10
让我们通过一个实际例子来巩固所学知识。假设我们需要解析一个简单的配置文件格式:
|function parseConfig(text) { let result = {}; let currentSection = result; text.split(/\r?\n/).forEach(line => { let match; if (match = line.match(/^(\w+)=(
|{ name: "网站配置", version: "1.0", database: { host: "localhost", port: "3306" }, cache: { enabled: "true", timeout: "30" } }
在JavaScript中,正则表达式对于非英文字符的处理存在一定的局限性。比如,\w 这个元字符在默认情况下只能匹配ASCII范围内的字母、数字以及下划线,并不会识别中文、日文等其他Unicode字符。如果你希望正则表达式能够正确地识别和处理这些Unicode字符,需要在正则表达式后面加上u标志,这样引擎才会按照Unicode规范来解析点号(.)、\w、\d等元字符的含义,从而支持更多的字符类型。
|console.log(/<.>/.test("<🌸>"));
|false
|console.log(/<.>/u.test("<🌸>"));
|true
在较新的JavaScript版本中,正则表达式引入了Unicode属性转义\p{...},这使得我们能够根据字符的Unicode属性来进行匹配。例如,可以直接判断一个字符是否属于汉字、拉丁字母、数字等特定的字符类别。这种方式极大地扩展了正则表达式对多语言文本的支持能力,让我们能够更精确地处理各种国际化字符。
|console.log(/\p{Script=Han}/u.test("中"));
|true
+符号表示什么含义?test方法检查字符串中是否包含数字test方法检查字符串中是否包含字母test方法检查字符串是否完全由数字组成|// 检查字符串中是否包含数字 let hasDigit = /\d/.test("房间号是208"); console.log(hasDigit); // 输出: true // 检查字符串中是否包含字母 let hasLetter = /[a-zA-Z]/.test("房间号是208"); console.log(hasLetter); // 输出: false(因为只有中文和数字) // 检查字符串是否完全由数字组成 let isAllDigits = /^\d
match或exec方法|// 提取年份 let yearText = "今年是2024年"; let yearMatch = yearText.match(/\d+/); console.log(yearMatch[0]); // 输出: "2024" // 提取价格(包含小数点) let priceText = "价格是99.5元"; let priceMatch = priceText.match(/\d+\.
replace方法和正则表达式|// 替换所有"苹果"为"橘子" let text1 = "我喜欢苹果,他也喜欢苹果"; let result1 = text1.replace(/苹果/g, "橘子"); console.log(result1); // 输出: "我喜欢橘子,他也喜欢橘子" // 将日期格式从斜杠改为横线 let date1 = "2024/03/15"; let result2 = date1.replace(/\//g