3.3 数值的扩展与修复
数值没有什么好扩展的,而且JavaScript的数值精度问题一向臭名昭著,要修复它们可不是一两行代码了事。先看扩展,我们只把目光集中于Prototype.js与mootools就行了。
Prototype.js 为它添加 8 个原型方法。Succ 是加 1,times 是将回调重复执行指定次数toPaddingString与上面提到字符串扩展方法pad作用一样,toColorPart是转十六进制,abs、ceil、floor、abs是从Math中偷来的。
mootools 的情况:limit 是从数值限定在一个闭开间中,如果大于或小于其边界,则等于其最大值或最小值,times与Prototype.js的用法相似, round是Math.round的增强版,添加了精度控制, toFloat、toInt是从window中偷来的,其他的则是从Math中偷来的。
在es5_shim.js这个库,它实现了ECMA262v5提到的一个内部方法toInteger。
// http://es5.github.com/#x9.4 // http://jsperf.com/to-integer var toInteger = function(n) { n = +n; if (n !== n) { // isNaN n = 0; } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { n = (n > 0 || -1) * Math.floor(Math.abs(n)); } return n; };
但依我看来都没什么意义,数值往往来自用户输入,我们一个正则就能判定它是不是一个“数”,是则直接Number(n)!
基于同样的理由,mass Framework对数字的扩展也是很少的,三个独立的扩展。
limit 方法:确保数值在[n1,n2]闭区间之内,如果超出限界,则置换为离它最近的最大值或最小值。
function limit(target, n1, n2) { var a = [n1, n2].sort(); if (target < a[0]) target = a[0]; if (target > a[1]) target = a[1]; return target; }
nearer方法:求出距离指定数值最近的那个数。
function nearer(target, n1, n2) { var diff1 = Math.abs(target - n1), diff2 = Math.abs(target - n2); return diff1 < diff2 ? n1 : n2 }
Number下唯一需要修复的方法是toFixed,它是用于校正精确度,最后的那个数会做四舍五入操作,但在一些浏览器中并没有这样干。
想简单修复可以这样处理:
if (0.9.toFixed(0) !== '1') { Number.prototype.toFixed = function(n) { var power = Math.pow(10, n); var fixed = (Math.round(this * power) / power).toString(); if (n == 0) return fixed; if (fixed.indexOf('.') < 0) fixed += '.'; var padding = n + 1 - (fixed.length - fixed.indexOf('.')); for (var i = 0; i < padding; i++) fixed += '0'; return fixed; }; }
追求完美的话,还存在这样一个版本,把里面的加、减、乘、除都重新实现了一遍。
if (!Number.prototype.toFixed || (0.00008).toFixed(3) !== '0.000' || (0.9).toFixed(0) === '0' || (1.255).toFixed(2) !== '1.25' || (1000000000000000128).toFixed(0) !== "1000000000000000128") { // 一些内部方法与变量,防止全局污染 (function() { var base, size, data, i; base = 1e7; size = 6; data = [0, 0, 0, 0, 0, 0]; function multiply(n, c) { var i = -1; while (++i < size) { c += n * data[i]; data[i] = c % base; c = Math.floor(c / base); } } function divide(n) { var i = size, c = 0; while (--i >= 0) { c += data[i]; data[i] = Math.floor(c / n); c = (c % n) * base; } } function toString() { var i = size; var s = ''; while (--i >= 0) { if (s !== '' || i === 0 || data[i] !== 0) { var t = String(data[i]); if (s === '') { s = t; } else { s += '0000000'.slice(0, 7 - t.length) + t; } } } return s; } function pow(x, n, acc) { return (n === 0 ? acc : (n % 2 === 1 ? pow(x, n - 1, acc * x) : pow(x * x, n / 2, acc))); } function log(x) { var n = 0; while (x >= 4096) { n += 12; x /= 4096; } while (x >= 2) { n += 1; x /= 2; } return n; } Number.prototype.toFixed = function(fractionDigits) { var f, x, s, m, e, z, j, k; // Test for NaN and round fractionDigits down f = Number(fractionDigits); f = f !== f ? 0 : Math.floor(f); if (f < 0 || f > 20) { throw new RangeError("Number.toFixed called with invalid number of decimals"); } x = Number(this); // Test for NaN if (x !== x) { return "NaN"; } // If it is too big or small,return the string value of the number if (x <= -1e21 || x >= 1e21) { return String(x); } s = ""; if (x < 0) { s = "-"; x = -x; } m = "0"; if (x > 1e-21) { // 1e-21 < x < 1e21 // -70 < log2(x) < 70 e = log(x * pow(2, 69, 1)) - 69; z = (e < 0 ? x * pow(2, -e, 1) : x / pow(2, e, 1)); z *= 0x10000000000000; // Math.pow(2, 52); e = 52 - e; // -18 < e < 122 // x = z / 2 ^ e if (e > 0) { multiply(0, z); j = f; while (j >= 7) { multiply(1e7, 0); j -= 7; } multiply(pow(10, j, 1), 0); j = e - 1; while (j >= 23) { divide(1 << 23); j -= 23; } divide(1 << j); multiply(1, 1); divide(2); m = toString(); } else { multiply(0, z); multiply(1 << (-e), 0); m = toString() + '0.00000000000000000000'.slice(2, 2 + f); } } if (f > 0) { k = m.length; if (k <= f) { m = s + '0.0000000000000000000'.slice(0, f - k + 2) + m; } else { m = s + m.slice(0, k - f) + '.' + m.slice(k - f); } } else { m = s + m; } return m; } }()); }
toFixed 方法实现得如此艰难其实也不能怪浏览器,计算机所理解的数字与我们是不一样的。众所周知,计算机的世界是2进制,数字也不例外。为了储存更复杂的结构,需要用到更高维的进制。而进制间的换算是存在误差的。虽然计算机在一定程度上反映了现实世界,但它提供的顶多只是一个“幻影”,经常与我们的常识产生偏差。例如,将1除以3,然后再乘以3,最后得到的值竟然不是1;10个0.1相加也不等于1;交换相加的几个数的顺序,却得到了不同的和。
console.log(0.1 + 0.2) console.log(Math.pow(2, 53) === Math.pow(2, 53) + 1) //true console.log(Infinity > 100) //true console.log(JSON.stringify(25001509088465005)) //25001509088465004 console.log(0.1000000000000000000000000001) //0.1 console.log(0.100000000000000000000000001) //0.1 console.log(0.1000000000000000000000000456) //0.1 console.log(0.09999999999999999999999) //0.1 console.log(1 / 3) //0.3333333333333333 console.log(23.53 + 5.88 + 17.64)// 47.05 console.log(23.53 + 17.64 + 5.88)// 47.050000000000004
这些其实不是BUG,而是我们无法接受这事实。在JavaScript中,数值有三种保存方式。
· 字符串形式的数值内容。
· IEEE754标准双精度浮点数,它最多支持小数点后带15~17位小数,由于存在2进制和10进制的转换问题,具体的位数会发生变化。
· 一种类似于C语言的init类型的32位整数,它由4个8 bit的字节构成,可以保存较小的整数。
当JavaScript遇到一个数值时,它会首先尝试按照整数来处理该数值,如行得通,则把数值保存为31 bit的整数;如果该数值不能视为整数,或超出31 bit的范围,则把数值保存为64位的IEEE 754浮点数。
聪明的读者一定想到了这样一个问题:什么时候规规矩矩的整数会突然变成捉摸不定的双精度浮点数?答案是:当它们的值变得非常庞大时,或者进入1和0之间时。因此,1和0是首先必须注意的两个数值。
接下来,最大的Unicode值是1114111(7位数字,相当于(/x41777777),而最大的RGB颜色值是16777215(8位数字,相当于#FFFFFF)。最大的32 bit带符号整数是2147483647(10位数字,即Math.pow(2,31)-1),-2147483648最小,所以JavaScript内部会以整数的形式保存所有Unicode值和 RGB 颜色。2147483647 是第三个必须注意的数值,任何大于该值的数据将保存为双精度格式。
9007199254740992(16位数字,即Math.pow(2,53))是最大的浮点数,输出时类似整数,所有Date对象(按毫秒计算)都小于该值,因此总是模拟整数的格式输出。它是第四个必须注意的数值。
最后,最大的双精度数值是1.7976931348623157e+308,超出这个范围就要算作无穷大了。
因此,我们就看出缘由了,大数相加出问题是由于精度的不足,小数相加出问题是进制转算时产生误差。第一个好理解。第二个,主要是我们常用的10进制转换为2进制时,变成循环小数及无理数等有无限多小数位的数,计算机要用位数有限的浮点数来表示是无法实现的,只能从某一位进行截短。而且,因为内部表示是2进制,10进制看起来是能除尽的数,往往在2进制是循环小数。比如用2进制来表示10进制的0.1,就得写成2的幂(因为小于1,所以幂是负数)相加的形式。若一直持续下去,0.1就成了0.000110011001100110011...这种循环小数。在有效数字的范围内进行舍入,就会产生误差。
综上所述,我们就尽量避免小数操作与大数操作,或者转交后台去处理,实在避免不了就引入专业的库来处理。