正则表达式,也称为正则表达式或正则表达式,是一个难以解决的问题。 如果您还没有 100% c 对编写自己的正则表达式感到满意,请不要感到羞耻,因为它确实需要一些时间来适应。 我希望在本文结束时,您将更接近在 JavaScript 中使用自己的表达式,而无需过多依赖 Stack Overflow 中的 copypasta。
编写正则表达式的第一步是了解如何调用它。 在 JavaScript 中,正则表达式是 标准的内置对象 。 因此,我们可以通过以下几种方式创建一个新的 RegExp
对象:
- 字面意思,
/expression/.match('string to test against')
- 带字符串参数的
new
关键字new RegExp('expression')
- 带文字的
new
关键字new RegExp(/expression/)
我将使用这些方法的组合来证明它们本质上执行相同的工作。
我们正则表达式的目标
在我的示例中,我将使用包含我的名字、姓氏和域名的字符串。 在现实世界中,这个例子需要更多的思考。 在 处理名称 时有许多微妙之处,我不会在这里讨论。
假设我正在构建一个仪表板并希望显示登录用户的名称。 我无法控制返回给我的数据,所以我必须使用我所拥有的。
我需要将 aaron.arney:alligator.io
转换为 Aaron Arney [Alligator]
。
正则表达式将许多逻辑融入到单个压缩对象中。 这可能而且将会引起混乱。 一个好的做法是将表达式分解为一种伪代码形式。 这使我们能够看到需要发生什么以及何时发生。
- 提取名字
- 提取姓氏
- 提取域名
- 将字符串格式化为所需的模板格式
First Last [Domain]
匹配名字
要将字符串与正则表达式匹配,您所要做的就是传递文字字符串。 表达式末尾的 i
是一个标志。 i
标志特别代表 case insensitive
。 这意味着我们的表达式忽略字符串上的大小写。
const unformattedName = 'aaron.arney:alligator.io'; const found = unformattedName.match(/aaron/i); console.log(found); // expected output: Array [ "aaron" ]
这很好用,但在我们的例子中,这不是一个好方法,因为用户的名字并不总是“Aaron”。 这是我们探索以编程方式匹配字符串的地方。
让我们暂时专注于匹配名字。 把单词分解成单个字符,你看到了什么?
“Aaron”这个名字由五个字母字符组成。 every 名字是否只有 五个字符? 不,但可以合理地假设名字可以在 1 到 15 个字符之间。 为了表示 az 范围内的字符,我们使用 [a-z]
。
现在,如果我们更新我们的表达式以使用这个字符类...…
const unformattedName = 'aaron.arney:alligator.io'; const found = unformattedName.match(/[a-z]/i); console.log(found); // expected output: Array [ "a" ]
它不是从字符串中提取“aaron”,而是只返回“a”。 这很好,因为正则表达式会尽可能少地匹配。 要重复字符匹配一个数字,直到我们的限制为 15,我们使用大括号。 这告诉我们观察的表达式匹配前面的标记,我们的“az”,匹配 1 到 15 次。
const unformattedName = 'aaron.arney:alligator.io'; const unformattedNameTwo = 'montgomery.bickerdicke:alligator.io'; const unformattedNameThree = 'a.lila:alligator.io'; const exp = new RegExp(/[a-z]{1,15}/, 'i'); const found = unformattedName.match(exp); const foundTwo = unformattedNameTwo.match(exp); const foundThree = unformattedNameThree.match(exp); console.log(found); // expected output: Array [ "aaron" ] console.log(foundTwo); // expected output: Array [ "montgomery" ] console.log(foundThree); // expected output: Array [ "a" ]
匹配姓氏
提取姓氏应该像复制和粘贴我们的第一个表达式一样简单。 您会注意到匹配仍然返回相同的值,而不是同时返回名字和姓氏。
逐个字符分解字符串,名称之间用句号分隔。 为了解决这个问题,我们在表达式中添加了句号。
我们必须在这里小心。 .
可以表示表达式中的两件事之一。
.
- 匹配除换行符以外的任何字符\.
- 匹配一个 .
在这种情况下使用任一版本都会产生相同的结果,但情况并非总是如此。 eslint 之类的工具有时会将转义序列 \
标记为不必要,但我说安全总比抱歉好!
const unformattedName = 'aaron.arney:alligator.io'; const exp = new RegExp(/[a-z]{1,15}\.[a-z]{1,15}/, 'i'); const found = unformattedName.match(exp); console.log(found); // expected output: Array [ "aaron.arney" ]
由于我们更喜欢将字符串拆分为两个项目,并且不包括表达式返回的句号,我们现在可以使用 capturing groups
。 这些由括号 ()
表示,并环绕您想要返回的表达式部分。 如果我们将它们包裹在名字和姓氏表达式周围,我们将得到新的结果。
使用捕获组的语法很简单:(expression)
。 由于我只想返回我的名字和姓氏并且 而不是 句号,因此将表达式括在括号中。
const unformattedName = 'aaron.arney:alligator.io'; const exp = new RegExp(/([a-z]{1,15})\.([a-z]{1,15})/, 'i'); const found = unformattedName.match(exp); console.log(found); // expected output: Array [ "aaron.arney", "aaron", "arney" ]
匹配域名
为了提取“alligator.io”,我们将使用到目前为止我们已经使用过的字符类。 当然,稍作修改。
验证域名和 TLD 是一项艰巨的任务。 我们将假设我们解析的域总是 > 3 && < 25
字符。 TLD 始终为 > 1 && < 10
。 如果我们插入这些,我们将得到一些新的输出:
const unformattedName = 'aaron.arney:alligator.io'; const exp = new RegExp(/([a-z]{1,15})\.([a-z]{1,15}):([a-z]{3,25}\.[a-z]{2,10})/, 'i'); const found = unformattedName.match(exp); console.log(found); // expected output: Array [ "aaron.arney:alligator.io", "aaron", "arney", "alligator.io" ]
捷径
我向你展示了关于表达的“漫长的道路”。 现在,我将向您展示如何使用不太冗长的表达式来捕获相同的文本。 通过使用 +
量词,我们可以告诉我们的表达式尽可能多地重复前面的标记。 它将一直持续到它遇到死胡同,在我们的例子中是句号。 这个表达式还引入了 g
标志,它代表 global
。 它告诉表达式我们想要尽可能多地重复我们的搜索,而不是最少的次数。
// With the global flag 'aaron.arney:alligator.io'.match(/[a-z]+/ig); // expected output: Array(4) [ "aaron", "arney", "alligator", "io" ] // Without the global flag 'aaron.arney:alligator.io'.match(/[a-z]+/i); // expected output: Array(4) [ "aaron" ]
格式化输出
要格式化字符串,我们将在 String
对象上使用 替换方法 。 replace
方法有两个参数:
RegExp | String
- 正则表达式对象或文字RegExp | function
- 正则表达式或函数
const unformattedName = 'aaron.arney:alligator.io'; // The "long" way const exp = new RegExp(/([a-z]{1,15})\.([a-z]{1,15}):([a-z]{3,25}\.[a-z]{2,10})/, 'i'); unformattedName.replace(exp, '$1 $2 [$3]'); // expected output: "aaron arney [alligator.io]" // A slightly shorter way unformattedName.replace(/([a-z]+)\.([a-z]+):([a-z]+\.[a-z]{2,10})/ig, '$1 $2 [$3]'); // expected output: "aaron arney [alligator.io]"
在上面的代码片段中,$1
、$2
、$3
是由 replace
方法解释的特殊模式。
$1
-The first result from the match array
=>A reference to the first parenthesized group
$2
-The second result from the match array
=>A reference to the second parenthesized group
$n
- 以此类推
为了使单词大写,我们可以使用另一个正则表达式。 我们将传递一个函数,而不是像上面那样格式化输出。 该函数将提供的参数大写并返回它。
在这里,我将介绍几个新部分,anchors
、alternation
和一个新的字符类 [^]
。
[^abc]
- 不是a
、b
或c
\b
- 字边界ab|cd
- 逻辑“或”,匹配ab
或cd
// Capitalize the words "aaron arney [alligator.io]".replace(/(^\b[a-z])|([^\.]\b[a-z])/g, (char) => char.toUpperCase()); // expected output: "Aaron Arney [Alligator.io]"
把这个表达式分成两部分…
(^\b[a-z])
- 捕获字符串的第一个字符。^
表示匹配字符串的开头。|([^\.]\b[a-z])
- 或者,匹配 不 以句点.
开头的新词,因为这是 TLD。
继续你的探索
这只是对正则表达式强大功能的一小部分了解。 我完成的示例是可以改进的,但是如何改进呢?
- 表达方式是否过于冗长? 是不是太简化了?
- 它是否涵盖边缘情况?
- 你能用本机方法的一些巧妙的字符串操作来替换它吗?
在这里,您可以获取所学知识并尝试回答这些问题。 探索以下资源,以帮助您的旅程和实验!