在JavaScript中字符串转数字
在JavaScript的世界里,字符串转数字是十分微妙的。面对NaN
、隐式基数(指使用原生JS parseInt()等方法转换字符串形式的数字而不指定进制时,默认的进制选择规则。译者注)、数字字面量和Number对象的种种概念和用法,稍不留神就会掉进自己挖的坑里。这篇文章里,我会讲述在转换成数字时,对用parseFloat()
还是 Number()
,用Number.isNaN()
还是isNaN()
之间怎么作出权衡。
另外,本文也会描述怎么用eslint来配置实行这些规则。
简单来讲,将一般的JavaScript的值转换为数字时,如果对被转换值的范围支持较为宽泛,那就应该用 Number(x)
,否则,用 parseFloat(x)
。在检查是否转换成功时,不要用全局对象的isNaN()
函数, 而应该用Number.isNaN()
。
typeof parseFloat('42'); // 'number'
Number.isNaN(Number('42')); // false
typeof parseFloat('fail'); // 'number'
Number.isNaN(Number('fail')); // true
用Number(x)做转换时,一些边界情况是否合理,取决于你如何处理它们。你也可以用类似 archetype 的工具来帮你处理一些边界值:
archetype.to('42', 'number'); // 42
// 对于空字符串的处理:
Number(''); // 0
archetype.to('', 'number'); // throws, 抛出异常
许多开发人员用 +x
来将字符串转换成数字。JavaScript语言规范中规定+x
与Number(x)
等价
+'42 fail'; // NaN
+({ valueOf: () => '42' }); // 42
+({ toString: () => '42' }); // 42
+(null); // 0
+(' '); // 0
Number(x)
有何不妥
Number(x)
和 parseFloat(x)
处理边界值的方式有很大差异。 parseFloat()
在处理某些字符串时,更为宽泛(能成功转换成数字):
Number('42 fail'); // NaN
parseFloat('42 fail'); // 42
parseInt('42 fail'); // 42
Number(' 10'); // 10
parseFloat(' 10'); // 10
parseInt(' 10'); // 10
你可能会误以为这就意味着用 Number(x)
处理边界值时更安全、更严格。不幸的是,处理空格、null
和其他边界值的时候,Number(x)
却相对没那么严格——很多值会被出乎意料地转换成0。举个几个栗子:
Number(null); // 0
Number(''); // 0
Number(' '); // 0
Number(false); // 0
Number({ toString: () => '' }); // 0
Number({ valueOf: () => ' ' }); // 0
这是因为JavaScript语言规范对于将不同的值转换成数字有一套相当复杂的规则。 parseFloat()
进行值转换的规则相对简单。解析引擎必须先将传入的值转换成字符串,去掉首尾空格,然后找出符合JavaScript正则表达式定义中的数字字面量的最长前置序列。
Number.isNaN()
与 isNaN()
还有另外一个关于将不同的值转换成数字的小坑,当尝试转换成数字失败的时候,JavaScript并不会抛出异常,而会返回一个特殊的值NaN
。更让人头大的是,用 typeof
运算符判断其类型时,得到的结果是'number'
。
Number('fail'); // NaN
typeof Number('fail'); // number
之所以有 Number.isNaN()
和 isNaN()
这两个东东,是因为==
和===
用来判断值相等时,如果任意一边存在NaN
,得出的结果都出乎意料。
Number('fail') == Number('fail'); // false
Number('fail') === Number('fail'); // false
Number('fail') == NaN; // false
NaN === NaN; // false
Number.isNaN()
是ES6的一个新特性,然而它没得到太多关注。Number.isNaN()
的鲁棒性更佳,你应该它用来替代isNaN()
,确实需要用到 isNaN()
的场合除外。
// 判断一个值是否数字,用 `=== NaN` ***行不通***,所以需要用函数来判断
isNaN(Number('fail')); // true
Number.isNaN(Number('fail')); // true
要便于区分两者,可以这么类比: Number.isNaN()
之于isNaN()
类似于===
之于 ==
。 isNaN()
函数在检查给定的值是否为NaN
之前会先将其转换成数字。
isNaN('fail'); // true
isNaN({}); // true
Number.isNaN('fail'); // false
Number.isNaN({}); // false
另一方面, 如果 x
不是数字型的值,Number.isNaN(x)
则返回 false
。你可以用以下函数写一个 Number.isNaN()
的polyfill:
Number.isNaN = function(x) {
return typeof x === 'number' && isNaN(x);
};
反过来看,isNaN(x)
与 Number.isNaN(Number(x))等价
。当你检查 Number(x)
或 parseFloat(x)
的结果是否为 NaN
的时候, 用isNaN(x)
是安全的。这是因为传入该函数的值已经是尝试转换为数字的结果。但通常来说,应该多用Number.isNaN()
而不是 isNaN()
,就跟除了真的需要用 ==
来判断两值相等,你一般情况下会用 ===
做判断的道理一样。
ESLint 规则
通过 eslint 的 no-restricted-globals
规则,可配置强制使用 Number.isNaN()
,以及配置选用 Number()
还是 parseFloat()
。在这个GitHub issue可查看更多信息。以下的例子是在 .eslintrc.yml
配置禁用全局对象的isNaN()
和parseFloat()
方法。
rules:
no-restricted-globals:
- error
- name: isNaN
message: Use `Number.isNaN()` instead
- name: parseFloat
message: Use `Number()` instead
如果要开启parseFloat()
,禁用 Number()
的话要麻烦一丢丢,可以用no-restricted-syntax
这条eslint规则来搞定。
rules:
no-restricted-globals:
- error
- name: isNaN
message: Use `Number.isNaN()` instead
no-restricted-syntax:
- error
- selector: CallExpression[callee.name='Number']
message: Do not use `Number()`, use `parseFloat()` instead
加以实践
在JavaScript中转数字,充满着奇奇怪怪的边界用例。如果你不想考虑这些情况,用 parseFloat()
和Number.isNaN()
的组合就最合适了。想灵活处理的话,就用Number()
。个人而言,因为无需检查转换结果是否为NaN
,我选择 archetype 。
原文链接:http://thecodebarbarian.com/convert-a-string-to-a-number-in-javascript.html
原文作者:Valeri Karpov
发表时间:2019年01月22日
网友评论