在网页中,我们从用户输入的内容中获取的值通常是字符串,但是有时候我们希望用户输入的内容一定要能转成数值:
userInput.addEventListener('change', (e) => {
const value = e.target.value;
console.log(typeof value); // string
console.assert(isNumeric(value), `Not a numeric value: ${value}`);
});
即我们要实现一个isNumeric方法,判断用户输入的值是能转为数值的字符串。
我们讨论isNumeric实现前,先说一下限制用户输入的方式。
👉🏻 如果我们设置input的type为number,并不能保证输入的内容一定是数值,因为如果input的type是number,它依然可以输入多个“+“、”-”、“.”、“e”。
<input type="number" step="0.0000001" id="userInput">
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O8MPKpMC-1648634434959)(https://camo.githubusercontent.com/445f8c431dc5585d2caed02cf7ca87116e63ac5bd11858281ae26f8604c5ad28/68747470733a2f2f70352e73736c2e7168696d672e636f6d2f743031613662336362316364383636303163382e6a7067)]
input[type=number]并不阻止输入多个e
这是因为“+/-”(正负符号),“.”(小数点)和“e”(科学记数法)都是Number允许输入的字符。
不过如果在form提交的时候,浏览器会对input[type=number]
内容再做一次检查:
<form id="myForm">
<input type="number">
<input type="submit">
</form>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RuaK3ofb-1648634434961)(https://camo.githubusercontent.com/063abcd18b735e9cdc25ff387625ed6f298b44f5a71ac5c4315782fdafb76baf/68747470733a2f2f70352e73736c2e7168696d672e636f6d2f743031633766303765336530316332303163332e6a7067)]
但是,不管怎样,用户还是可以通过修改页面上的元素,绕过这些检查,所以我们还是要用到isNumeric来判断用户输入的合法性。
我们先看一下isNumeric应该返回什么。
如果参考input[type=number]的规则,那么它应该支持所有合法的有穷数值写法:
function isNumeric(str) {
...
}
console.assert(isNumeric('1000'));
console.assert(isNumeric('-100.'));
console.assert(isNumeric('.1'));
console.assert(isNumeric('-3.2'));
console.assert(isNumeric('001'));
console.assert(isNumeric('+4.5'));
console.assert(isNumeric('1e3'));
console.assert(isNumeric('1e-3'));
console.assert(isNumeric('-100e-3'));
console.assert(!isNumeric('++3'));
console.assert(!isNumeric('-100..'));
console.assert(!isNumeric('3abc'));
console.assert(!isNumeric('abc'));
console.assert(!isNumeric('-3e3.2'));
console.assert(!isNumeric('Infinity'));
console.assert(!isNumeric('-Infinity'));
console.assert(!isNumeric(''));
那么具体要怎么实现呢?
parseFloat?
有同学想到用parseFloat,这个行不行呢?
function isNumeric(str) {
return !Number.isNaN(parseFloat(str));
}
这个显然是不行的,因为parseFloat('123abc')
结果是123,因为parseFloat会尝试转部分数值,而忽略掉不能转数值的部分。
所以:
console.assert(!isNumeric('-100..'));
console.assert(!isNumeric('3abc'));
console.assert(!isNumeric('-3e3.2'));
这三个case是过不去的,另外这里用了Number.isNaN
处理parseFloat之后的结果,由于±Infinity是数值,Number.isNaN
会返回false,所以:
console.assert(!isNumeric('Infinity'));
console.assert(!isNumeric('-Infinity'));
也pass不了。
isNaN
有同学说,那我们直接使用isNaN如何?
function isNumeric(str) {
return !isNaN(str);
}
这次结果好得多,但是最后三条规则过不了:
console.assert(!isNumeric('Infinity'));
console.assert(!isNumeric('-Infinity'));
console.assert(!isNumeric(''));
±Infinity和上面的原因一样,但是为什么''
也pass不了呢?这是因为isNaN会先尝试将参数转为Number,而空字符串被转为了数值0。
console.log(Number('')); // 0
这里面就不得不提一下ECMA-262规范里面[[ToNumber]]
的转换规则了:
根据规则,Null、Boolean都会转成Number,Undefined被转成NaN,Undefined会被转成NaN,而Symbol直接抛TypeError…
加上空字符串''
被转成0,isNaN就会有些怪异的行为了:
console.log(isNaN(undefined)); // true
console.log(isNaN(null)); // false
console.log(isNaN(true)); // false
console.log(isNaN(false)); // false
console.log(isNaN('')); // false
其实字符串除了''
还有一些:
console.log(isNaN(' ')); // false
console.log(isNaN(' ')); // false
console.log(isNaN('\t')); // false
console.log(isNaN('\r')); // false
console.log(isNaN('\n')); // false
这就是为什么ES2015之后,又增加了Number.isNaN方法。
👉🏻 冷知识:isNaN方法对参数做[[ToNumber]]
转换,会导致一些比较怪异的结果,所以ES2015增加了Number.isNaN,该方法不会对参数做类型转换,只要参数不是NaN,不管是什么类型,Number.isNaN一律返回false。
console.log(isNaN('abc')); // true
console.log(Number.isNaN('abc')); // false
console.log(isNaN('')); // false
console.log(Number.isNaN('')); // false
isFinite
我们把isNaN换成isFinite看看:
function isNumeric(str) {
return isFinite(str);
}
这下'±Infinity'
的问题解决了,因为Number中的±Infinite和NaN的isFinite结果都返回false。
不过与isNaN一样,isFinite也一样会对参数进行类型转换,所以,这几个case问题还是存在:
console.assert(!isNumeric(''));
console.assert(!isNumeric(' '));
console.assert(!isNumeric(' '));
console.assert(!isNumeric('\t'));
console.assert(!isNumeric('\r'));
console.assert(!isNumeric('\n'));
👉🏻 冷知识:isFinite与isNaN一样,会对参数做[[ToNumber]]
转换,因此对应的,ES2015也提供了一个Number.isFinite
,这是不转换参数类型的版本。如果参数不是Number类型,Number.isFinite
一律返回false。
console.log(isFinite('123')); // true
console.log(Number.isFinite('123')); // false
console.log(isFinite('')); // true
console.log(Number.isFinite('')); // false
好了,那么讨论到这里,最后的解决方法已经呼之欲出了。
因为对于isNumeric用法,我们只需要处理字符串,非字符串的case我们可以不管;那么我们剩下的就是处理这一堆字符串case:
console.assert(!isNumeric(''));
console.assert(!isNumeric(' '));
console.assert(!isNumeric(' '));
console.assert(!isNumeric('\t'));
console.assert(!isNumeric('\r'));
console.assert(!isNumeric('\n'));
这个有很多方式可以处理了,比如它们都匹配正则/^\s*$/
,所以:
function isNumeric(str) {
return !/^\s*$/.test(str) && isFinite(str);
}
这个版本就可以通过所有的case了。
另外,这些字符串的parseFloat都是NaN,所以,也可以这样:
function isNumeric(obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj);
}
实际上这个比上面那个正则的版本更好,因为这个还同时处理了非字符串的case,因为:
parseFloat(null);
parseFloat(true);
parseFloat(false);
上面这些的结果都是NaN。
实际上,上面这个版本就是著名的jQuery框架中的jQuery.isNumeric
的实现方式。
因为现在不建议用isNaN和isFinite,而推荐使用Number.isNaN
和Number.isFinite
替代,所以一些linter的规则可能会禁止使用这两个函数,但是没有关系,因为我们可以这么写:
function isNumeric(obj) {
return !Number.isNaN(parseFloat(obj))
&& Number.isFinite(Number(obj));
}
所以,这个就是最终的版本。
原来,实现一个小小的函数isNumeric,有那么多需要注意的地方。
关于判断字符串是数值,你还有什么想法,欢迎在issue中讨论。