JavaScirpt 货币转换成千分位正则 (非捕组获匹配详解)
如果给你一串数字,需要把他转换成货币的千分位格式,你会如何去做?比如:123123123 -> 123,123,123
1. 一个有意思的正则表达式的由来
这其实是个陈年老问题了,但是不知为何最近的出镜率特别高,所以决定这里讨论一下。
先看一种传统的思维:从右侧起每隔三位加一个逗号。于是就有了下面的方法:
function money(num){
// 先把数字换成字符串,然后转换成数组,反转之后,再组合成字符串
var reverseStr = num.toString().split('').reverse().join('');
// 用正则替换,每隔3位加一个逗号
reverseStr = reverseStr.replace(/(\d{3})/g,'$1,');
// 处理正好三位的情况,如 123 -> ,123
reverseStr = reverseStr.replace(/\,$/,'');
// 把加了逗号的字符串反转回正常的顺序
reverseStr = reverseStr.split('').reverse().join('');
return reverseStr;
}
虽然这个方法能满足我们的需求,但是或多或少感觉有些low,也不是我们今天讨论的重点。
我们今天尝试使用一句简短的正则搞定这个问题,先上代码:
function money(num){
return (''+num).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
}
很简单的一个正则/(\d)(?=(\d{3})+(?!\d))/
就搞定了一切。正则虽然短,但是并不简单,今天的目的,其实就是和大家一起来研究研究这个正则的内容。
我们先讨论这里涉及到的3个概念:
- 正则匹配的
lastIndex
- 正则中形如
(?=exp)
的零宽度正预测先行断言
- 正则中形如
(!=exp)
的零宽度负预测先行断言
1.1 lastIndex
lastIndex
:指的是上一次匹配的结果位置,也就是下一次匹配开始的位置,默认情况下为0(需要强调的是,只有在匹配模式是g或者y的情况下才有效,否则,每次匹配完成之后,lastIndex都会变成0)。这里之所以强调lastIndex是因为后面的断言(也叫非捕获)匹配会影响lastIndex的值,我们先看例子:
let re = /\d\d/g;
let str = '0123456789';
console.log(re.lastIndex);
// 0
// 默认情况下 lastIndex 的值为0
console.log(re.exec(str));
// ["01",...] 匹配到了01
console.log(re.lastIndex);
// 2
// 由于第一次匹配的结果是01,接下来的匹配应该从01之后的2开始,所以此时的lastIndex为2
同理,如果也可以手动修改lastIndex的值,匹配的结果也会受到影响。
let re = /\d\d/g;
let str = '0123456789';
re.lastIndex = 5;
// 手动修改 lastIndex 的值为5,下次匹配从第五未开始
console.log(re.exec(str));
// ["56",...]
console.log(re.lastIndex);
// 7 匹配完成之后,lastIndex自动修改到匹配的结果之后
1.2 (?=exp) 零宽度正预测先行断言 与 (?!exp) 零宽度负预测先行断言
第一次听到这个名字的感觉就好像不会中文一样,不知所云,内心一万只羊驼奔过。
在定义上它是指:它断言自身出现的位置的后面能匹配表达式exp
,[一脸懵逼]表示我第一次没有看懂。
还是举一个简单的例子帮助大家理解,假设有人告诉你请你去找一个骑车的人,然后把这个人带过来(车不需要)
你会怎么做?这句话抽象出来就其实就是用人骑车
去匹配正则/人(?=骑车)/
,结果要的是人而不需要车。
也正是由于上面括号内的表达式对结果没有影响,他们也属于非捕组获匹配
。
举个例子
var re = /ap(?=ple)/g;
console.log(re.exec('I like apple not app!'));
// ["ap", index: 7, input: "I like apple not app!"]
console.log(re.lastIndex)
// 9
console.log(re.exec('I like apple not app!'));
// null
console.log(re.lastIndex)
// 0
在上述例子中,第一个.exec会找到句子中ple之前的ap,那么第7-11个字符apple
就符合我们的条件,但是由于(?=ple)是非捕获的,所以ple的并没有被计算到结果中,自然ple这3个字符也没有影响到lastIndex,所以lastIndex的值为 7+2=9 ,而不是7+5=11;
这里的非捕获很容忍让人误解,所以再强调一遍:
!!非捕获不会影响lastIndex的值!!
如果你明白了,请在脑海想一下下面的2个题目的结果是什么:
题目一
var re = /ap(?=ple)pie/g;
console.log(re.test('applepie'))
题目二
var re = /ap(?=ple)plepie/g;
console.log(re.test('applepie'))
不要作弊哦
答案是 ↓ ↓ ↓
题目一:false
题目二:true
如果没有猜对的话我们一起来看一下为什么:
实际上我们可以把/ap(?=ple)pie/
分成2部分 ap(?=ple)
+ pie
,在匹配字符串applepie
的时候,经过了以下的步骤:
- 一开始lastIndex的值为0,
ap(?=ple)
匹配applepie
中的apple
的ap
字段,此时的结果是ap
,lastIndex=2。 - 第一步过后lastIndex=2,后面的表达式是
pie
,表示从第二个字符开始后面应该紧接着的是pie
,但是实际上第二个字符后面的是ple
,这样条件就满足不了了,所以返回的就是false。 同理第二个正则/ap(?=ple)plepie/
的第二步表示的是从第二个字符开始后面紧跟着的是plepie
,完全符合我们的给出的字符串,所以结果是true
。
对于(?!exp) 零宽度负预测先行断言
就不用多说了,他表示后面不跟着exp,同样也是非捕获(比如:.+?(?!xyz)
去匹配uvw
和uvwxyz
的话只会匹配第一个)
2. /(\d)(?=(\d{3})+(?!\d))/的匹配步骤
说到这里如果你还是很迷茫的话,那么我们再说详细举一个例子来说明上面的正则是如何工作的。
我们以'1234567.88'.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')
为例。
首先解释一下(?=(\d{3})+(?!\d))
的意思,他表示某个匹配规则之后,有一个或者多个数字组+
,每组由3个数字\d{3}
组成(比如 123456,123,123456789,这些都是由个数字组组成的字符串),并且数字组之后不是数字(?!\d)
(这个是用来找到结尾,只要后面不是数字我们都认为是结尾)。
最后我们用图表来解释这个过程:
index | (\d)也就是$1 的值 | (?=(\d{3})+(?!\d))匹配的结果 | lastLindex | 字符串结果 |
---|---|---|---|---|
0 | '1' | (234)(567) | 1 | '1,234567' |
1 | '2' | -- (24567无法分成2租) | 2 | '1,234567' |
2 | '3' | -- (4567无法分成2租) | 3 | '1,234567' |
3 | '4' | (567) | 4 | '1,234,567' |
... | ... | ... | ... | ... |
9 | '8' | -- | 10 | '1,234,567' |
好了,就说这么多了,如果还有疑问的话,可以再后面给我留言。