前端中的数字

450 阅读6分钟

在前台界面中,前端通常都要对数字进行处理,例如:取整;取两位小数;字符串转数字等等

NaN 非数字

在 JavaScript 中,NaN 代表 "Not-a-Number"(不是一个数字),它是一个特殊的值,用于表示一个未定义或不可表示的数值结果,注意它并非是个类型。 NaN给用户的体验是不好的,我们需要避免出现这样子的结果,通常在以下情况下会产生 NaN:

  • 数学运算错误:当进行非法的数学运算时,例如 0 除以 0
let result = 0 / 0;
  • 无效的数值转换:当尝试将无法转换为数值的字符串转换为数值时
let result = Number("abc");
  • 操作数为 NaN:任何涉及 NaN 的运算结果都是 NaN
let result = NaN + 5;

怎么判断一个值是不是 NaN呢? 由于 NaN 不等于任何值,包括它自身,不能使用 == 或 === 来检测 NaN。可以使用 isNaN() 函数或 Number.isNaN() 方法来检测。

NaN === NaN  // false

NaN == NaN // false

isNaN() 函数会先尝试将参数转换为数值,然后检测是否为 NaN。

console.log(isNaN(NaN)); // true
console.log(isNaN("abc")); // true
console.log(isNaN(123)); // false

强制类型转换,这意味着它可能会返回一些意外的结果,例如:

isNaN(NaN); // true
isNaN(undefined); // true
isNaN({}); // true

Number.isNaN() 方法不会进行类型转换,只在参数确实是 NaN 时返回 true。

console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("abc")); // false
console.log(Number.isNaN(123)); // false

所以,如果需要判断NaN时,建议使用 Number.isNaN

JavaScript 的 Number 对象

前面讲完了非数字,现在开始讲数字,在 JavaScript 中,只有一种数字类型,可以使用也可以不使用小数点来书写数字。例如

let num1 = 100;
let num2 = 3.14;

let billNum = 3e5; // 科学计数法
 

由于科学计数法的允许,导致 input 标签 type="number" 时,仍可输入 +,-,e 符号;

精度问题

JavaScript 中,实际上是不区分 整数和浮点数的,所有的数字都是 采用 IEEE754 标准定义的 64 位(双精度)浮点格式。其中 0 到 51 存储数字(片段),52 到 62 存储指数,63 位存储符号。由于存储的位数是有限的,而有些浮点数字转成二进制的时候位数是无限的,所以必然会存在截断,这就造成了JavaScript中的经典问题 0.1 + 0.2 = 0.30000000000000004;

(0.1).toString(2) //  '0.0001100110011001100110011001100110011001100110011001101'
二进制转化步骤
  • 0.1 转成二级制 0.1 × 2 = 0.2,整数部分是 0,小数部分是 0.2。 0.2 × 2 = 0.4,整数部分是 0,小数部分是 0.4。 0.4 × 2 = 0.8,整数部分是 0,小数部分是 0.8。 0.8 × 2 = 1.6,整数部分是 1,小数部分是 0.6。 0.6 × 2 = 1.2,整数部分是 1,小数部分是 0.2。 0.2 × 2 = 0.4,整数部分是 0,小数部分是 0.4。 0.4 × 2 = 0.8,整数部分是 0,小数部分是 0.8。 0.8 × 2 = 1.6,整数部分是 1,小数部分是 0.6。 0.6 × 2 = 1.2,整数部分是 1,小数部分是 0.2。 可以看到,小数部分开始重复了,这意味着 0.1 的二进制表示是一个无限循环小数。 结果:000110011……

  • 13 转成二级制 13 ÷ 2 = 6,余数是 1。 6 ÷ 2 = 3,余数是 0。 3 ÷ 2 = 1,余数是 1。 1 ÷ 2 = 0,余数是 1。 结果:1011

由上面的转化过程可以看出,精度问题只存在小数的计算,整数的计算不会出现这种问题,在不得已必须前台做计算时,可以通过将小数转换为整数进行运算,然后再转换回小数。

let result = 0.1 + 0.2;
console.log(Math.round(result * 100) / 100); // 输出 0.3

区间问题

上面提到存储的位数是有限的,那么JavaScript中能表示的数值最大值和最小值肯定也是有上限的。

console.log(Number.MAX_VALUE) // 1.7976931348623157e+308
console.log(Number.MIN_VALUE) // 5e-324

进制问题

如果前缀为 0,则 JavaScript 会把数值常量解释为八进制数,如果前缀为 0 和 "x",则解释为十六进制数

var y = 0377;
var z = 0xFF;

默认情况下,JavaScript 数字为十进制显示。

但是你可以使用 toString() 方法 输出16进制、8进制、2进制

var myNumber=128;
myNumber.toString(16);   // 返回 80
myNumber.toString(8);    // 返回 200
myNumber.toString(2);    // 返回 10000000

这里需要注意的一个问题是,不要使用 1.toString(2) 因为这里的. 会被当做小数点,所以应该写成 (1).toString(2) 或者 1..toString(2)

new Number

数字可以是数字也可以是对象;数字可以私有数据进行初始化,就像 x = 123;

JavaScript 数字对象初始化数据, var y = new Number(123);

var x = 123;
var y = new Number(123);
typeof(x) // 返回 Number
typeof(y) // 返回 Object

x === y // false

数字的操作

对于数字常见的算术运算操作是 加、减、乘、除、取余、指数

let a = 10;
let b = 3;

console.log(a % b); // 输出: 1
console.log(a ** b); // 输出: 1000

数学函数方法:

绝对值:Math.abs()
向上取整:Math.ceil()
向下取整:Math.floor()
四舍五入:Math.round()
取整:Math.trunc()
最大值:Math.max()
最小值:Math.min()
平方根:Math.sqrt()
幂运算:Math.pow()
随机数:Math.random()

常见的方法:

方法描述
Number.parseFloat()将字符串转换成浮点数,和全局方法 parseFloat() 作用一致。
Number.parseInt()将字符串转换成整型数字,和全局方法 parseInt() 作用一致。
Number.isFinite()判断传递的参数是否为有限数字。
Number.isInteger()判断传递的参数是否为整数。
Number.isNaN()判断传递的参数是否为 NaN
Number.isSafeInteger()判断传递的参数是否为安全整数。

将字符串转成数字,我们也可以使用 Number 构造函数,那么 它和 parseFloat的区别呢?

let num3 = parseFloat('123abc'); // 123
let num4 = parseFloat('abc123'); // NaN

console.log(Number('123abc')); // 输出: NaN
console.log(Number('abc123')); // 输出: NaN

Number():更严格,只要字符串中有任何非数字字符(除了前导和尾随空格),就会返回 NaN parseFloat():更宽松,只要字符串以数字开头,就会解析该数字,忽略后续的非数字字符。

vue的 指令 v-model修饰符 .number

在vue的内置指令 v-model 含有修饰符 .number.lazy 等,但是这个 .number 好像没那么的好用,我们看看源码一探究竟

/**
 * "123-foo" will be parsed to 123
 * This is used for the .number modifier in v-model
 */
export const looseToNumber = (val: any): any => {
  const n = parseFloat(val)
  return isNaN(n) ? val : n
}

由于vue中使用的是 parseFloat 是非严格模式,所以 输入框中 是可以 输入 ``123abc这种字符串的,只是绑定的值会被转成123` 而已

数字格式化

指定小数位

toFixed(),数字类型将转成字符串,使用时记得做非空校验,也可以 num?.toFixed(2)

let num = 1234.56789;

console.log(num.toFixed(2)); // 输出: "1234.57"


金额格式化

金额格式化,我们一般会采用封装好的工具函数库,当然我们也可以使用原生api自己封装,使用到的api:Intl.NumberFormat

let num = 1234567.89;

// 默认格式
let formatter = new Intl.NumberFormat();
console.log(formatter.format(num)); // 输出: "1,234,567.89"

// 指定语言和选项
formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});
console.log(formatter.format(num)); // 输出: "\$1,234,567.89"

formatter = new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR'
});
console.log(formatter.format(num)); // 输出: "1.234.567,89 €"

formatter = new Intl.NumberFormat('en-IN', {
  maximumSignificantDigits: 3
});
console.log(formatter.format(num)); // 输出: "1,230,000"

其他复杂格式

对于复杂的格式化,我们可以采用正则的方式替换,或者自行封装,下面是几个举例:

function formatNumberWithCommas(num) {
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

let num = 1234567.89;
console.log(formatNumberWithCommas(num)); // 输出: "1,234,567.89"
function formatAsPercentage(num, decimals = 2) {
  return (num * 100).toFixed(decimals) + '%';
}

let num = 0.1234;
console.log(formatAsPercentage(num)); // 输出: "12.34%"