本文参考JavaScript高级程序设计第四版 第三章 语言基础 3.4.5节
一、Number类型
1.八进制和十六进制
八进制第一位必须是0,第二位必须是0-7.超出范围将做为十进制来解析。
let octalNum1 = 070;//八进制56
let octalNum2 = 079;//无效八进制,解析为79
let octalNum3 = 08;//无效八进制,解析为8
十六进制前两位必须是0x,后跟0-9或者A-F(大小写均可)
let hexNum1 = 0xA; // 十六进制 10
let hexNum2 = 0x1f; // 十六进制 31
2.小数
因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小数点后面跟着 0(如 1.0),那它也会被转换为整数,如下例所示:
let floatNum1 = 1.; // 小数点后面没有数字,当成整数 1 处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。科学记数法用于表示一个应该乘以10 的给定次幂的数值。ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10 的多少次幂。比如:
let floatNum = 3.125e7; // 等于 31250000
在这个例子中,floatNum 等于 31 250 000,只不过科学记数法显得更简洁。这种表示法实际上相当于说:“以 3.125 作为系数,乘以 10 的 7 次幂。”
科学记数法也可以用于表示非常小的数值,例如 0.000 000 000 000 000 03。这个数值用科学记数法可以表示为 3e-17。默认情况下,ECMAScript 会将小数点后至少包含 6 个零的浮点值转换为科学记数法(例如,0.000 000 3 会被转换为 3e-7)。
浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。例如,0.1 加 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。比如下面的例子:
if (a + b == 0.3) { // 别这么干!
console.log("You got 0.3.");
}
这里检测两个数值之和是否等于 0.3。如果两个数值分别是 0.05 和 0.25,或者 0.15 和 0.15,那没问题。但如果是 0.1 和 0.2,如前所述,测试将失败。因此永远不要测试某个特定的浮点值。
扩展参考js 小数的精度损失
3.NAN
有一个特殊的数值叫 NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执行。但在 ECMAScript 中,0、+0 或-0 相除会返回 NaN:
console.log(0/0); // NaN
console.log(-0/+0); // NaN
如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity 或-Infinity:
console.log(5/0); // Infinity
console.log(5/-0); // -Infinity
NaN 有几个独特的属性。首先,任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),在连续多步计算时这可能是个问题。其次,NaN 不等于包括 NaN 在内的任何值。例如,下面的比较操作会返回 false:
console.log(NaN == NaN); // false
为此,ECMAScript 提供了 isNaN()函数。该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。把一个值传给 isNaN()后,该函数会尝试把它转换为数值。某些非数值的值可以直接转换成数值,如字符串"10"或布尔值。任何不能转换为数值的值都会导致这个函数返回true。举例如下:
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10 是数值
console.log(isNaN("10")); // false,可以转换为数值 10
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值 1
上述的例子测试了 5 个不同的值。首先测试的是 NaN 本身,显然会返回 true。接着测试了数值 10和字符串"10",都返回 false,因为它们的数值都是 10。字符串"blue"不能转换为数值,因此函数返回 true。布尔值 true 可以转换为数值 1,因此返回 false。
4.数值转换
有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个函数执行的操作也不同。
5.Number()函数
Number()函数基于如下规则执行转换。
- 布尔值,true 转换为 1,false 转换为 0。
- 数值,直接返回。
- null,返回 0。
- undefined,返回 NaN。
- 字符串,应用以下规则。
- 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此,Number("1")返回 1,Number("123")返回 123,Number("011")返回 11(忽略前面的零)。
- 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
- 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
- 如果是空字符串(不包含字符),则返回 0。 如果字符串包含除上述情况之外的其他字符,则返回 NaN。
- 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用toString()方法,再按照转换字符串的规则转换。
从不同数据类型到数值的转换有时候会比较复杂,看一看 Number()的转换规则就知道了。下面是几个具体的例子:
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
可以看到,字符串"Hello world"转换之后是 NaN,因为它找不到对应的数值。空字符串转换后是 0。字符串 000011 转换后是 11,因为前面的零被忽略了。最后,true 转换为 1。
注意 本章后面会讨论到的一元加操作符与 Number()函数遵循相同的转换规则。
6.parseInt()
考虑到用 Number()函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用 parseInt()函数。
parseInt()函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number()不一样,它返回 0)。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。比如,"1234blue"会被转换为 1234,因为"blue"会被完全忽略。类似地,"22.5"会被转换为 22,因为小数点不是有效的整数字符。
假设字符串中的第一个字符是数值字符,parseInt()函数也能识别不同的整数格式(十进制、八进制、十六进制)。换句话说,如果字符串以"0x"开头,就会被解释为十六进制整数。如果字符串以"0"开头,且紧跟着数值字符,在非严格模式下会被某些实现解释为八进制整数。
下面几个转换示例有助于理解上述规则:
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
不同的数值格式很容易混淆,因此 parseInt()也接收第二个参数,用于指定底数(进制数)。如果知道要解析的值是十六进制,那么可以传入 16 作为第二个参数,以便正确解析:
let num = parseInt("0xAF", 16); // 175
事实上,如果提供了十六进制参数,那么字符串前面的"0x"可以省掉:
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN
在这个例子中,第一个转换是正确的,而第二个转换失败了。区别在于第一次传入了进制数作为参数,告诉 parseInt()要解析的是一个十六进制字符串。而第二个转换检测到第一个字符就是非数值字符,随即自动停止并返回 NaN。
通过第二个参数,可以极大扩展转换后获得的结果类型。比如:
let num1 = parseInt("10", 2); // 2,按二进制解析
let num2 = parseInt("10", 8); // 8,按八进制解析
let num3 = parseInt("10", 10); // 10,按十进制解析
let num4 = parseInt("10", 16); // 16,按十六进制解析
因为不传底数参数相当于让 parseInt()自己决定如何解析,所以为避免解析出错,建议始终传给它第二个参数。注意 多数情况下解析的应该都是十进制数,此时第二个参数就要传入 10。
7.parseFloat()
parseFloat()函数的工作方式跟 parseInt()函数类似,都是从位置 0 开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5"将转换成 22.34。
parseFloat()函数的另一个不同之处在于,它始终忽略字符串开头的零。这个函数能识别前面讨论的所有浮点格式,以及十进制格式(开头的零始终被忽略)。十六进制数值始终会返回 0。因为parseFloat()只解析十进制值,因此不能指定底数。最后,如果字符串表示整数(没有小数点或者小数点后面只有一个零),则 parseFloat()返回整数。下面是几个示例:
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
二、参考js中使用位操作符取整有没有什么副作用?
1.Number.parseInt 默认接收两个参数
第一个参数是默认是 string 类型值,如果不是,会通过抽象的 ToString 强制转化成 string 类型的值。这其中就会有强制类型转换过程中的各种坑。
第二个参数是 number 类型的进制,如果不是,会通过抽象的 ToNumber 强制转化成 number 类型的值,范围是 2-36,通过强制类型转换后如果是其他值会返回 NaN。在 ES5 之前如果没有传入这个参数,会根据第一个参数的开头来判断进制,0 开头的字符串会判断成八进制,也就是 @貘吃馍香 提到的老黄历坑。ES5 之后已经解决,不传这个参数默认十进制。但是这个参数容易被忽略,尤其是在和 map 之类的也容易忽略后续可选参数的函数搭配使用的时候,比如['10', '10', '10', '10'].map(parseInt) // 结果是 [10, NaN, 2, 3]
所以如果只是用 parseInt 来 “取整”,一个良好的习惯是永远记得设置第二个参数为 10
然后就是尽量不要拿 parseInt 去转换一些其他类型的值,如果实在遇到了需要判断结果(比如一些闲的蛋疼的面试官非得要考察这种)那就先对两个参数进行求值,并转换成相应的类型,然后判断。判断的过程可以大致理解为:第一个参数的转换结果去除空白,然后从左往右提取出在第二个参数指定的进制下能够理解的整数部分,并返回这个值在十进制下的值。如果转换失败返回 NaN除了这些情况,parseInt 在 JavaScript 数值允许的范围内都是可以安全使用的
parseInt("1234blue")//1234
parseInt("");//NaN
parseInt(22.5)//22
parseInt("70")//70
parseInt("070")//56
parseInt("08")//0(8是无效的八进制)
parseInt("09")//0(9是无效的八进制)
parseInt("0xA")//10
parseInt("0xf")//15
三、参考为什么 parseInt(0.0000008) === 8?
parseInt接受两个参数,第一个参数是要转换的字符串(忽略空白);第二个参数是多少进制。建议总是加上第二个参数。
parseInt(1000000000000000000000.5, 10); // 1
为什么会这样呢?
parseInt 的第一个类型是字符串,所以会将传入的参数转换成字符串,也就是String(1000000000000000000000.5) 的结果为 '1e+21'。parseInt 并没有将 'e' 视为一个数字,所以在转换到 1 后就停止了。
这也就可以解释 parseInt(0.0000008) === 8
String(0.000008); // '0.000008'
String(0.0000008); // '8e-7'
从上面的程式码可以看出,小于 0.0000001(1e-7) 的数字转换成 String时,会变成科学记号法,再对这个数进行 parseInt操作就会导致这个问题发生。
结论:不要将 parseInt 当做转换 Number 和 Integer 的工具。
再补上一些悲剧:
parseInt(1/0, 19); // 18
parseInt(false, 16); // 250
parseInt(parseInt, 16); // 15
parseInt("0x10"); // 16
parseInt("10", 2); // 2
1.parseFloat
遇到第一个小数点有效,后面的无效。parseFloat没有第二个参数,只解析十进制
parseFloat("22.34.5")//22.34
parseFloat("1234blue")//1234
parseFloat("0xA")//0
parseFloat("22.5")//22.5
parseFloat("0908.5")//908.5
parseFloat("3.125e7")//31250000