- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
导读
在 JavaScript 中处理文本数据时,"从一个字符串里找出所有数字"是最常见的需求之一,而难点在于数字的形态多样:可能是整数2、带小数点的浮点数-1.5、也可能带负号-123.4。本文基于 en.javascript.info 教程中的经典练习,从零构造一条可复用的十进制数字正则表达式/-?\d+(\.\d+)?/g,并深入讲解捕获组、量词作用域、可选组等底层原理,最终你能独立写出并调试出处理整数、浮点数、负数混合文本的匹配方案。
一、任务要求:匹配所有十进制数字
本题来自正则表达式章节"捕获组"一节的第三个练习(任务原文),要求如下:
编写一个正则表达式,查找所有十进制数字,包括整数、带浮点小数的数字以及负数。
给出的使用示例:
let regexp = /your regexp/g; let str = "-1.5 0 2 -123.4."; alert( str.match(regexp) ); // -1.5, 0, 2, -123.4观察测试字符串"-1.5 0 2 -123.4."可以发现三个隐含要求:
- 整数(
0、2)也要被匹配,不能要求必须带小数点; - 负数(
-1.5、-123.4)的负号-属于数字的一部分,需要一并捕获; - 末尾的孤立句点(
-123.4.最后的.)不能被误当成小数部分——即小数点后必须跟数字才算小数。
二、分步构造正则表达式
第 1 步:匹配整数部分\d+
在字符类一节中我们知道,\d表示"任意单个数字(0–9)"。而一个整数是"一个或多个连续数字",因此在量词一节中,+表示"一次或多次"(等价于{1,}):
let regexp = /\d+/g;" -1.5 0 2 -123.4."用它会匹配出1、5、0、2、123、4这样一堆零散数字——显然不符合要求。
第 2 步:用捕获组加上可选小数部分(\.\d+)?
浮点数的结构是"整数部分 + 点 + 小数部分"。点.在正则里是特殊字符(匹配除换行外的任意字符),必须用\.转义。小数部分\.\d+表示"一个点后跟至少一位数字"。
关键一步是把它用圆括号包起来:(\.\d+)?。这里同时用到了捕获组的两个核心作用(见捕获组文章):
- 分组:把
\.\d+视为一个整体; - 量词作用域:其后的
?(零次或一次,等价于{0,1})作用于整个括号组,而不是只作用于某个字符。
如果没有括号,写成\.\d+?,量词?只会作用于\d+,含义变成"点后跟一个或多个数字,但尽可能少",完全是另一种语义。
于是得到"带可选小数的正数":\d+(\.\d+)?。
第 3 步:加上可选的负号-?
负数需要在数字前出现一个-。-?表示"零个或一个负号",置于最前面:
let regexp = /-?\d+(\.\d+)?/g;这就是官方答案中的完整模式。逐块拆解:
| 片段 | 含义 |
|---|---|
-? | 可选的一个负号(负数) |
\d+ | 整数部分:一个或多个数字 |
(\.\d+)? | 可选的小数部分:捕获组包裹的"点 + 一个或多个数字" |
三、运行验证与匹配结果分析
用测试字符串验证:
let regexp = /-?\d+(\.\d+)?/g; let str = "-1.5 0 2 -123.4."; alert( str.match(regexp) ); // -1.5, 0, 2, -123.4四个匹配分别是:
-1.5:负号 + 整数1+ 小数.5;0:整数,无负号无小数,-?与(\.\d+)?都选择了"零次";2:同上,纯整数;-123.4:负号 + 整数123+ 小数.4。
特别注意字符串末尾的-123.4.:最后一个孤立句点没有被匹配进结果。原因正是(\.\d+)?要求点后必须跟至少一位数字,末尾句点后没有数字,量词?选择"零次",该句点自然被跳过。这体现了用(\.\d+)?而非\.?\d*之类的宽松写法的价值——后者会错误地把孤立句点也算进数字。
四、边界情况与注意点
从正则语义可以推断出这条模式的行为边界(解决方案及上文的分析):
- 不匹配
+.5这类以正号开头或只有小数点的写法:模式中没有+?,小数点也必须前有整数部分。若需要支持.5或+5,需自行扩展模式(例如[+-]?\d*\.?\d+变体),但那样会引入新的歧义(如孤立句点),属于题目范围外的权衡。 - 千分位、科学计数法(如
1e3、1,000.5)不在匹配范围:题目仅要求十进制数字的三种基本形态。 -的位置:模式中-?直接接在开头。若文本中还有2-3这样的减法表达式,-会被当作下一个负数的符号——这是"负号"与"减号"在正则层面的天然歧义,是否接受取决于业务场景。
若需要精确提取数字但排除前缀匹配,可以结合\b词边界使用(如/\b-?\d+(\.\d+)?\b/g),但需注意\b对-和.的边界判定,实际项目中应先用测试用例验证。
五、作为构建块复用:解析算术表达式
这条数字正则并不孤立——在本仓库"捕获组"一节的后续练习 Parse an expression 中,它被直接复用为解析"1.2 * 3.4"、"-3 / -6"等算术表达式的数字部分:
function parse(expr) { let regexp = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/; let result = expr.match(regexp); if (!result) return []; result.shift(); return result; } alert( parse("-1.23 * 3.45") ); // -1.23, *, 3.45这里正是把-?\d+(\.\d+)?作为"一个数字"的原子模式,配合捕获组把第一个数字、运算符、第二个数字分别提取为结果数组的元素。可以看到:把"数字"抽象成一条成熟的子模式,是构建复杂解析正则的基础能力,这也是为什么本题值得单独练习。
六、进阶:非捕获组与命名组
在上一节代码中出现了(?:\.\d+)?,这是非捕获组:圆括号仍然把\.\d+聚合成一个整体供?使用,但不会在结果数组中额外占用一个索引。相关原理见捕获组文章中的"Non-capturing groups with ?:"小节。
对比两种写法:
// 捕获组版本:小数部分会出现在 match 数组里 let m1 = "1.5".match(/(-?\d+(\.\d+)?)/); // m1[1] === "1.5", m1[2] === ".5" // 非捕获组版本:小数部分不占索引 let m2 = "1.5".match(/(-?\d+(?:\.\d+)?)/); // m2[1] === "1.5"若需要语义更清晰的结果,还可以用命名组替代编号:
let regexp = /(?<num>-?\d+(?:\.\d+)?)/; let result = "1.5".match(regexp); alert(result.groups.num); // 1.5另外,当带g标志做全局匹配时,str.match不会返回各组内容;若需要同时拿到每个数字及其小数部分,应改用str.matchAll(参见捕获组文章中"Searching for all matches with groups: matchAll"一节):
let results = "-1.5 0 2 -123.4".matchAll(/(-?\d+(\.\d+)?)/g); for (let result of results) { alert(result[0]); // 完整数字,如 -1.5 alert(result[1]); // 整个数字(外层组) alert(result[2]); // 小数部分,如 ".5";整数时为 undefined }总结
- 核心模式:
/-?\d+(\.\d+)?/g可同时匹配整数、浮点数和负数,其关键是"整数部分\d++ 捕获组包裹的可选小数(\.\d+)?+ 可选负号-?"。 - 捕获组的作用:圆括号让量词作用于整体,并把组内容作为独立项返回(原理详见本仓库文章)。
- 工程复用:该模式作为"数字"原子,可直接用于算术表达式解析(示例);配合非捕获组
(?:...)、命名组(?<name>...)与matchAll,可以构建出结构清晰、便于提取结果的实际解析代码。
建议在你的项目中把这条模式封装成常量或工具函数(例如const DECIMAL_RE = /-?\d+(?:\.\d+)?/g;),并用包含"-1.5 0 2 -123.4."这类边界字符串的测试用例锁定行为,避免后续扩展时引入孤立句点、千分位等误匹配。
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考