JavaScript基础之数值计算
Posted bradenhan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript基础之数值计算相关的知识,希望对你有一定的参考价值。
常见的几种场景
场景一:进行浮点值运算结果的判断
常见错误写法:floatNum1 + floatNum2 === res
我们在Chrome里测试一下 0.1 + 0.2 === 0.3
,得出的结果是false,而不是预期结果true,因为 0.1 + 0.2 === 0.30000000000000004
场景二 :将小数乘以10的n次方取整(比如将元转化成分)
常见错误写法:parseInt(yuan*100, 10)
我们在Chrome里测试一下 parseInt(0.58*100, 10)
,得出的结果是57,而不是预期结果58。
场景三: 四舍五入保留n位小数
常见错误写法: (number).toFixed(2)
我们在Chrome里测试一下(1.335).toFixed(2)
,得出的结果是1.33,而不是预期结果1.34。
可以发现,稍有不注意,浮点数运算就会出问题,为什么会出现以上问题呢?
更多精彩内容,请微信搜索“前端爱好者
“, 戳我 查看 。
问题原因
在 JavaScript 中整数和浮点数都属于 Number
数据类型,所有数字都是以 64 位浮点数形式储存,即便整数也是如此。
所以我们在打印 1.00
这样的浮点数的结果是 1
而非 1.00
。
在一些特殊的数值表示中,例如金额,这样看上去有点变扭,但是至少值是正确了。
然而要命的是,当浮点数做数学运算的时候,你经常会发现一些问题,
举几个例子:
// 加法=====================
// 0.1 + 0.2 = 0.30000000000000004
// 0.7 + 0.1 = 0.7999999999999999
// 0.2 + 0.4 = 0.6000000000000001
//2.22 + 0.1 = 2.3200000000000003
// 减法 =====================
// 1.5 - 1.2 = 0.30000000000000004
// 0.3 - 0.2 = 0.09999999999999998
// 乘法 =====================
// 19.9 * 100 = 1989.9999999999998
// 19.9 * 10 * 10 = 1990
// 1306377.64 * 100 = 130637763.99999999
// 1306377.64 * 10 * 10 = 130637763.99999999
// 0.7 * 180 = 125.99999999999999
// 9.7 * 100 = 969.9999999999999
// 39.7 * 100 = 3970.0000000000005
// 除法 =====================
// 0.3 / 0.1 = 2.9999999999999996
// 0.69 / 10 = 0.06899999999999999
问题的原因解析
似乎是不可思议。小学生都会算的题目,JavaScript 不会?我们来看看其真正的原因。
JavaScript 里的数字是采用 IEEE 754 标准的 64 位双精度浮点数。
该规范定义了浮点数的格式,对于64位的浮点数在内存中的表示,最高的1位是符号位,接着的11位是指数,剩下的52位为有效数字,具体:
-
第0位:符号位, s 表示 ,0表示正数,1表示负数;
-
第1位到第11位:储存指数部分, e 表示 ;
-
第12位到第63位:储存小数部分(即有效数字),f 表示,
如图:
符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。
IEEE 754规定,有效数字第一位默认总是1,不保存在64位浮点数之中。
也就是说,有效数字总是1.xx…xx的形式,其中xx..xx的部分保存在64位浮点数之中,最长可能为52位。
因此,JavaScript提供的有效数字最长为53个二进制位(64位浮点的后52位+有效数字第一位的1)。
计算过程
比如在 JavaScript 中计算 0.1 + 0.2
时,到底发生了什么呢?
首先,十进制的0.1
和0.2
都会被转换成二进制,但由于浮点数用二进制表达时是无穷的,例如。
0.1 -> 0.0001100110011001...(无限)
0.2 -> 0.0011001100110011...(无限)
IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位,所以两者相加之后得到二进制为:
0.0100110011001100110011001100110011001100110011001100
因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了 0.30000000000000004
。所以在进行算术计算时会产生误差。
整数的精度问题
在 Javascript 中,整数精度同样存在问题,先来看看问题:
console.log(19571992547450991);
//=> 19571992547450990
console.log(19571992547450991===19571992547450992);
//=> true
同样的原因,在 JavaScript 中 Number
类型统一按浮点数处理,整数是按最大54位来算最大(253 - 1
,Number.MAX_SAFE_INTEGER
,9007199254740991
) 和最小(-(253 - 1)
,Number.MIN_SAFE_INTEGER
,-9007199254740991
) 安全整数范围的。
所以只要超过这个范围,就会存在被舍去的精度问题。
当然这个问题并不只是在 Javascript 中
才会出现,几乎所有的编程语言都采用了 IEEE-745 浮点数表示法,
任何使用二进制浮点数的编程语言都会有这个问题,
只不过在很多其他语言中已经封装好了方法来避免精度的问题,
而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。
浮点数计算 解决方案
上面说了这么多问题和原因,这里给出一些解决方案。
类库
通常这种对精度要求高的计算都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算问题。
前端也有几个不错的类库:
Math.js
Math.js 是专门为 JavaScript 和 Node.js 提供的一个广泛的数学库。
它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型 像数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。
功能强大,易于使用。
GitHub:https://github.com/josdejong/mathjs
decimal.js
为 JavaScript 提供十进制类型的任意精度数值。
官网:http://mikemcl.github.io/decimal.js/
GitHub:https://github.com/MikeMcl/decimal.js
big.js
官网:http://mikemcl.github.io/big.js
GitHub:https://github.com/MikeMcl/big.js/
这几个类库帮我们解决很多这类问题,不过通常我们前端做这类运算通常只用于表现层,应用并不是很多。所以很多时候,一个函数能解决的问题不需要引用一个类库来解决。
下面介绍各个更加简单的解决方案。
Javascript 数值计算 解决方案
整数表示
对于整数,我们可以通过用String
类型的表示来取值或传值,否则会丧失精度。
格式化数字、金额、保留几位小数等
如果只是格式化数字、金额、保留几位小数等可以查看这里 http://www.css88.com/archives/7324
浮点数运算
toFixed() 方法
浮点数运算的解决方案有很多,这里给出一种目前常用的解决方案, 在判断浮点数运算结果前对计算结果进行精度缩小,因为在精度缩小的过程总会自动四舍五入。
toFixed() 方法使用定点表示法来格式化一个数,会对结果进行四舍五入。语法为:
numObj.toFixed(digits)
参数 digits
表示小数点后数字的个数;介于 0 到 20 (包括)之间,实现环境可能支持更大范围。如果忽略该参数,则默认为 0。
返回一个数值的字符串表现形式,不使用指数记数法,而是在小数点后有 digits
位数字。
该数值在必要时进行四舍五入,另外在必要时会用 0 来填充小数部分,以便小数部分有指定的位数。
如果数值大于 1e+21
,该方法会简单调用 Number.prototype.toString()
并返回一个指数记数法格式的字符串。
特别注意:toFixed() 返回一个数值的字符串表现形式。
具体可以查看 MDN中的说明,那么我们可以这样解决精度问题:
parseFloat((数学表达式).toFixed(digits)); // toFixed() 精度参数须在 0 与20 之间
// 运行parseFloat((1.0 - 0.9).toFixed(10)) // 结果为 0.1
parseFloat((0.3 / 0.1).toFixed(10)) // 结果为 3
parseFloat((9.7 * 100).toFixed(10)) // 结果为 970
parseFloat((2.22 + 0.1).toFixed(10)) // 结果为 2.32
在老版本的IE浏览器(IE 6,7,8)中,toFixed()
方法返回值不一定准确。所以这个方法以前很少用。
以至于网上搜索出来的结果大都是下面这些方法。
其他的解决方案
还有一些其他的解决方案,简单的说需要将浮点数转换字符串,分隔成为整数部分和小数部分,小数部分再转换为整数,计算结果后,再转换为浮点数。这过程有点复杂…,网上找一下:
加法函数
/**
** 加法函数,用来得到精确的加法结果
** 说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。
** 调用:accAdd(arg1,arg2)
** 返回值:arg1加上arg2的精确结果
**/
function accAdd(arg1, arg2)
var r1, r2, m, c;
try
r1 = arg1.toString().split(".")[1].length;
catch (e)
r1 = 0;
try
r2 = arg2.toString().split(".")[1].length;
catch (e)
r2 = 0;
c = Math.abs(r1 - r2);
m = Math.pow(10, Math.max(r1, r2));
if (c > 0)
var cm = Math.pow(10, c);
if (r1 > r2)
arg1 = Number(arg1.toString().replace(".", ""));
arg2 = Number(arg2.toString().replace(".", "")) * cm;
else
arg1 = Number(arg1.toString().replace(".", "")) * cm;
arg2 = Number(arg2.toString().replace(".", ""));
else
arg1 = Number(arg1.toString().replace(".", ""));
arg2 = Number(arg2.toString().replace(".", ""));
return (arg1 + arg2) / m;
//给Number类型增加一个add方法,调用起来更加方便。
Number.prototype.add = function (arg)
return accAdd(arg, this);
;
减法函数
/**
** 减法函数,用来得到精确的减法结果
** 说明:javascript的减法结果会有误差,在两个浮点数相减的时候会比较明显。这个函数返回较为精确的减法结果。
** 调用:accSub(arg1,arg2)
** 返回值:arg1加上arg2的精确结果
**/
function accSub(arg1, arg2)
var r1, r2, m, n;
try
r1 = arg1.toString().split(".")[1].length;
catch (e)
r1 = 0;
try
r2 = arg2.toString().split(".")[1].length;
catch (e)
r2 = 0;
m = Math.pow(10, Math.max(r1, r2)); //last modify by deeka //动态控制精度长度
n = (r1 >= r2) ? r1 : r2;
return ((arg1 * m - arg2 * m) / m).toFixed(n);
// 给Number类型增加一个mul方法,调用起来更加方便。
Number.prototype.sub = function (arg)
return accMul(arg, this);
;
乘法函数
/**
** 乘法函数,用来得到精确的乘法结果
** 说明:javascript的乘法结果会有误差,在两个浮点数相乘的时候会比较明显。这个函数返回较为精确的乘法结果。
** 调用:accMul(arg1,arg2)
** 返回值:arg1乘以 arg2的精确结果
**/
function accMul(arg1, arg2)
var m = 0, s1 = arg1.toString(), s2 = arg2.toString();
try
m += s1.split(".")[1].length;
catch (e)
try
m += s2.split(".")[1].length;
catch (e)
return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
// 给Number类型增加一个mul方法,调用起来更加方便。
Number.prototype.mul = function (arg)
return accMul(arg, this);
;
除法函数
/**
** 除法函数,用来得到精确的除法结果
** 说明:javascript的除法结果会有误差,在两个浮点数相除的时候会比较明显。这个函数返回较为精确的除法结果。
** 调用:accDiv(arg1,arg2)
** 返回值:arg1除以arg2的精确结果
**/
function accDiv(arg1, arg2)
var t1 = 0, t2 = 0, r1, r2;
try
t1 = arg1.toString().split(".")[1].length;
catch (e)
try
t2 = arg2.toString().split(".")[1].length;
catch (e)
with (Math)
r1 = Number(arg1.toString().replace(".", ""));
r2 = Number(arg2.toString().replace(".", ""));
return (r1 / r2) * pow(10, t2 - t1);
//给Number类型增加一个div方法,调用起来更加方便。
Number.prototype.div = function (arg)
return accDiv(this, arg);
;
参考文档
javascript 之基础
(function(){ 全局属性 Infinity 表示正无穷大的数值 NaN 表示不是数值的值 undefined undefind值 全局函数 decodeURI() 解码使用encodeURI()转义的字符串 decodeURIComponent() 解码使用encodeURIComponent()转义的字符串 encodeURI() 通过转义特定字符对URI编码 encodeURIComponent 通过转义特定字符对URI的组成部分编码 escape() 用转义序列替换特定字符来对字符串编码 eval() 执行JavaScript代码字符串,返回结果 isFinite() 判断一个值是否无穷大 isNaN() 判断一个值是否是非数值 parseFloat() 从字符串中解析数值 parseInt() 从字符串中解析整数 unescape() 解码使用escape()编码的字符串 全局对象 Array Array()构造函数 Boolean Boolean()构造函数 Date Date()构造函数 Error Error()构造函数 EvalError EvalError()构造函数 Function Function()构造函数 JSON 引用一个对象,该对象定义了解析和序列化JSON的函数 JSON = JSONObj; Math 引用一个对象,该对象定义了数学函数 Math = MathObj Number Number()构造函数 Object Object()构造函数 RangeError RangeError()构造函数 ReferenceError ReferenceError()构造函数 RegExp RegExp()构造函数 String String()构造函数 SyntaxError SyntaxError()构造函数 TypeError TypeError()构造函数 URIError URIError()构造函数 Math Math.ceil() 对一个数字向上取整 Math.float(num) 对一个数字向下取整 Math.round() 四舍五入 Math.random() 计算一个随机数 Math.max() 返回两个数中较大的那个 Math.min() 返回两个数中较小的那个 Math.abs(num) 计算绝对值 Math.acos() 计算反余弦值 Math.asin() 计算反正弦值 Math.atan() 计算反正切值 Math.atan2() 计算从X轴到指定点的角度 Math.cos() 计算余弦值 Math.exp() 计算e的乘方 Math.log() 计算自然对数 Math.pow() 计算x的y次方 Math.sin() 计算正弦值 Math.sqrt() 计算平方根 Math.tan() 计算正切值 Number Number.toLocaleString() 将一个数字转换为本地格式的字符串 Number.toPrecision() 格式化一个数字的有效数字 Number.toString() 将一个数字转换为字符串 Number.valueOf() 返回原始的数值 Object hasOwnProperty() 检查对象是否拥有一个指定名字的本地定义(而不是继承)的属性 isPrototypeOf() 检查当前对象是不是指定对象的原型 propertyIsEnumerable() 检查指定名字的属性是否存在并且可以用for/in循环枚举 toLocaleString() 返回该对象的一个本地化的字符串表示 toString() 返回该对象的一个字符串表示 valueOf() 返回当前对象的原始值,如果存在原始值的话 静态方法 Object.create() 使用指定的原型及属性创建一个新的对象 Object.defineProperties() 创建或配置指定对象的一个或多个属性 Object.defineProperty() 创建或配置指定对象的某个属性 Object.freeze() 将指定对象设置为不可改变 Object.getOwnPropertyDescriptor() 查询指定对象的指定属性的特性 Object.getOwnPropertyNames() 返回一个包含指定对象的所有非继承属性名的数组,包括不可枚举属性 Object.getPrototypeOf() 返回指定对象的原型 Object.isExtensible() 检查当前对象是否能添加到新的属性中 Object.isFrozen() 检查当前对象是否已冻结 Object.isSealed() 检查指定对象是否为封闭的(sealed) Object.keys() 返回一个包含指定对象的所有非继承可枚举属性名的数组 Object.preventExtensions() 阻止向指定对象添加新的属性 Object.seal() 阻止向指定对象添加新的属性或删除现有属性 String charAt() 取出一个字符串中指定位置的字符 charCodeAt() 返回一个字符串中指定位置的字符的编码 concat() 将一个或多个值链接成一个字符串 indexOf() 在指定字符串中寻找一个字符或子串 lastIndexOf() 在指定字符串中向后寻找一个字符或子串 localeCompare() 使用本地定义的顺序比较字符串 match() 使用正则表达式执行模式匹配 replace() 使用正则表达式执行查找与替换操作 search() 在一个字符串中查找匹配某个正则表达式的子串 slice() 返回字符串的一个切片或子串 split() 在指定的分隔符字符串或正则表达式处断开,将一个字符串分割为由字符串组成的数组 substr() 提取字符串的一个子串,substring()的一个变体 substring() 提取字符串的一个子串 toLowerCase() 返回指定字符串的一份副本,其中所有的字符都已转为小写 toString() 返回原始的字符串值 toUpperCase() 返回指定字符串的一份副本,其中所有的字符都已转为大写 trim() 返回指定字符串的一份副本,其中前后的空白字符都已删除 valueOf() 返回原始的字符串值 静态方法 String.fromCharCode() 使用作为参数传入的字符编码创建一个新的字符串 Array concat() 把元素衔接到数组中 every() 测试断言函数是否对每个数组元素都为真 filter() 返回满足断言函数的数组元素 forEach() 为数组的每一个元素调用指定函数 indexOf() 在数组中查找匹配元素 join() 将数组的所有元素都转化为字符串,并衔接起来 lastIndexOf() 在数组中方向查找 map() 从数组的元素中,计算出新的数组元素 pop() 移除数组最后一个元素 push() 把元素添加到数组尾部 reduce() 从数组的元素中,计算出一个值 reduceRight() 从右到左缩减数组 reverse() 在原数组中颠倒数组元素的顺序 shift() 移除数组的第一个元素 slice() 返回数组的一部分 some() 测试是否至少有一个数组元素能让断言函数为真 sort() 在原数组中对数组元素进行排序 splice() 插入、删除或替换数组元素 toLocaleString() 将数组转化为本地化字符串 toString() 将数组转化为字符串 unshift() 在数组头部插入元素 Boolean Boolean.toString() 将布尔值转换成字符串 Boolean.valueOf() 返回布尔值的原始值 Function apply() 将函数作为指定对象的方法来调用。传递给它的是指定的参数数组 bind() 返回一个新函数。通过可选的中指定参数,作为指定对象的方法调用该方法 call() 将函数作为指定对象的方法来调用。传递给它的是指定的参数 toString() 返回函数的字符串表示 Date getDate() 返回Date对象的月份中的日期值,本地或世界时间 getDay() 返回Date对象的一周中的日期值, getFullYear() 返回日期的年份,完整的4位数的格式 getHours() 返回Date对象的小时值, getMilliseconds() 返回Date对象的毫秒值, getMinutes() 返回Date对象的分钟值 getMonth() 返回Date对象的月份值 getSeconds() 返回Date对象的秒数值 getTime() 返回Date对象内部毫秒表示形式。注意这个值与时区无关 getTimezoneOffset() 返回当前日期的本地表示与UTC表示之间相差的分钟数。注意返回值依赖于指定日期的夏令时是否有效 setDate() 设置日期的月份的日期值 setFullYear() 设置日期的年份(以及可选的月份及日期)值 setHours() 设置日期的小时值(以及可选的分钟、秒以及毫秒值) setMilliseconds() 设置日期的毫秒值 setMinutes() 设置日期的分钟值(以及可选的秒以及毫秒值) setMonth() 设置日期的月份值(以及可选的月份中的天数) setSeconds() 设置日期的秒值(以及可选的毫秒值) setTime() 使用毫秒的格式,设置一个Date对象的值 toDateString() 返回一个表示当前日期的日期部分的字符串 toISOString() 将一个Date转为字符串,使用ISO-8601标准来组合日期/时间格式和UTC toJSON() 将一个Date对象JSON序列化 toLocaleDateString() 返回一个表示当前日期的日期部分的字符串 toLocaleString() 将一个Date转换为字符串 toLocaleTimeString() 返回一个表示当前日期的时间部分的字符串 toString() 使用本地时区将一个Date转换为字符串 toTimeString() 返回一个表示指定日期的时间部分的字符串 toUTCString() 将一个Date转为字符串 valueOf() 将一个Date转为对应的内部毫秒格式 静态方法 Date.now() 返回当前时间,自纪元开始后的毫秒数 Date.parse() 解析一个日期及时间的字符串表示,返回该日期的内部毫秒表示 Date.UTC() 返回指定的UTC日期及时间的毫秒表示 JSON JSON.parse() 解析JSON格式的字符串 JSON.stringify() 序列化对象、数组或原始值 BOM DOM })();
以上是关于JavaScript基础之数值计算的主要内容,如果未能解决你的问题,请参考以下文章