問題重現 Javascript的數字類型只有一個number,沒有短型、整型和浮點型、雙浮點型等類型。由於Javascript在實現數字運算的時候,所採取的的浮點數類型實現方式,其會精確到小數點後16位。見下面的樣本:
7*0.8 = 5.6000000000000005
0.1+0.2 = 0.3000000000000001
解決思路 要解決這個問題一般有兩種方案,一種是用將數字轉化為字串來進行計算,另一種是將所有小數轉化為整數進行計算後再將計算結果轉化為對應的小數。我們主要採取第一種解決方案。
實現字串相加、相乘的運算 實現字串的相加相乘,有時候在某訊的面試題中會出現。下面我會完整實現,有可最佳化的地方可指出。
console.log(bigMut("567", "1234")); // 699678function bigMut(big, common) {big += "";common += "";if (big.length < common.length) {big = [common, common = big][0];}big = big.split("").reverse();var oneMutManyRes = [];var i = 0,len = big.length;for (; i < len; i++) {oneMutManyRes[oneMutManyRes.length] = oneMutMany(big[i], common) + getLenZero(i);}var result = oneMutManyRes[0];for (i = 1, len = oneMutManyRes.length; i < len; i++) {result = bigNumAdd(result, oneMutManyRes[i]);}return result;}function getLenZero(len) {len += 1;var ary = [];ary.length = len;return ary.join("0");}function oneMutMany(one, many) {one += "";many += "";if (one.length != 1) {one = [many, many = one][0];}one = parseInt(one, 10);var i = 0,len = many.length,resAry = [],addTo = 0,curItem,curRes,toSave;many = many.split("").reverse();for (; i <= len; i++) {curItem = parseInt(many[i] || 0, 10);curRes = curItem * one + addTo;toSave = curRes % 10;addTo = (curRes - curRes % 10) / 10;resAry.unshift(toSave);}if (resAry[0] == 0) {resAry.splice(0, 1);}return resAry.join("");}function bigNumAdd(big, common) {big += "";common += "";var maxLen = Math.max(big.length, common.length),bAry = big.split("").reverse(),cAry = common.split("").reverse(),i = 0,addToNext = 0,resAry = [],fn,sn,sum;for (; i <= maxLen; i++) {fn = parseInt(bAry[i] || 0);sn = parseInt(cAry[i] || 0);sum = fn + sn + addToNext;addToNext = (sum - sum % 10) / 10;resAry.unshift(sum % 10);}if (resAry[0] == 0) {resAry.splice(0, 1);}return resAry.join("");}
其實,如果我們將整套的解決方案(包括加、減、乘、除)封裝為一個庫,然後供每個人方便的調用,那將是最好的。實現如下。
實現庫 整個的庫的實現代碼以及使用API如下:
/* * 小數計算 * @example: * 0.1+0.2 //0.30000000000000004 * var a=Decimal('0.1');var b=Decimal('0.2'); * a.add(b).toNumber() //0.3 * * 四捨五入,保留一位小數 * a.add(b).add(0.14).toNumber(1) //0.4 * * Decimal.add(0.1,0.2,0.3).toNumber() //0.6 * Decimal.add([0.1,0.2,0.3]).toNumber() //0.6 * * (0.1+0.2+0.3)*2/0.5 //2.4000000000000004 * Decimal.add([0.1,0.2,0.3]).mul(2).div(0.5).toNumber() //2.4 * */(function (ROOT, factory) { if (typeof exports === 'object') { // Node. module.exports = factory(); } else if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(factory); } else { // Browser globals (root is window) ROOT.Decimal = factory(); }}((0,eval)(this), function () { var DECIMAL_SEPARATOR = '.'; // Decimal var Decimal = function (num) { if (this.constructor != Decimal) { return new Decimal(num); } if (num instanceof Decimal) { return num; } this.internal = String(num); this.as_int = as_integer(this.internal); this.add = function (target) { var operands = [this, new Decimal(target)]; operands.sort(function (x, y) { return x.as_int.exp - y.as_int.exp }); var smallest = operands[0].as_int.exp; var biggest = operands[1].as_int.exp; var x = Number(format(operands[1].as_int.value, biggest - smallest)); var y = Number(operands[0].as_int.value); var result = String(x + y); return Decimal(format(result, smallest)); }; this.sub = function (target) { return Decimal(this.add(target * -1)); }; this.mul = function (target) { target = new Decimal(target); var result = String(this.as_int.value * target.as_int.value); var exp = this.as_int.exp + target.as_int.exp; return Decimal(format(result, exp)); }; this.div = function (target) { target = new Decimal(target); var smallest = Math.min(this.as_int.exp, target.as_int.exp); var x = Decimal.mul(Math.pow(10, Math.abs(smallest)), this); var y = Decimal.mul(Math.pow(10, Math.abs(smallest)), target); return Decimal(x / y); }; this.toString = function (precision) { if (isNumber(precision)) { return ''+toFixed(Number(this.internal), precision); } return this.internal; }; this.toNumber = function (precision) { if (isNumber(precision)) { return toFixed(Number(this.internal), precision); } return Number(this.internal); } }; var as_integer = function (number) { number = String(number); var value, exp, tokens = number.split(DECIMAL_SEPARATOR), integer = tokens[0], fractional = tokens[1]; if (!fractional) { var trailing_zeros = integer.match(/0+$/); if (trailing_zeros) { var length = trailing_zeros[0].length; value = integer.substr(0, integer.length - length); exp = length; } else { value = integer; exp = 0; } } else { value = parseInt(number.split(DECIMAL_SEPARATOR).join(''), 10); exp = fractional.length * -1; } return { 'value': value, 'exp': exp }; }; // Helpers var neg_exp = function (str, position) { position = Math.abs(position); var offset = position - str.length; var sep = DECIMAL_SEPARATOR; if (offset >= 0) { str = zero(offset) + str; sep = '0.'; } var length = str.length; var head = str.substr(0, length - position); var tail = str.substring(length - position, length); return head + sep + tail; }; var pos_exp = function (str, exp) { var zeros = zero(exp); return String(str + zeros); }; var format = function (num, exp) { num = String(num); var func = exp >= 0 ? pos_exp : neg_exp; return func(num, exp); }; var zero = function (exp) { return new Array(exp + 1).join('0'); }; var methods = ['add', 'mul', 'sub', 'div']; for (var i = 0; i < methods.length; i++) { (function (method) { Decimal[method] = function () { var args = [].slice.call(arguments); if (isArray(args[0])) { args = args[0]; } if (args.length == 1) { return new Decimal(args[0]); } var option = args[args.length - 1]; var sum = new Decimal(args[0]), index = 1; while (index < args.length) { sum = sum[method](args[index]); index++; } return sum; }; })(methods[i]); } var toFixed = function (number, precision) { var multiplier = Math.pow(10, precision + 1), wholeNumber = Math.floor(number * multiplier); return Math.round(wholeNumber / 10) * 10 / multiplier; }; var isNumber = function (o) { return Object.prototype.toString.call(o).slice(8, -1) === 'Number'; }; var isArray = function (o) { return Object.prototype.toString.call(o).slice(8, -1) === 'Array'; }; var isObject = function (o) { return Object.prototype.toString.call(o).slice(8, -1) === 'Object'; }; return Decimal;}));
這種簡單的封裝有兩個比較好的地方需要特別指明一下。
間接調用eval 間接調用eval顧名思義就是不是直接調用eval。間接調用eval和直接調用eval的一個(僅有的一個)區別就是:間接調用eval所執行的範圍始終是在全域,它不會以某個函數或對象為執行範圍來對要執行的字串進行求值。樣本如下: var a = {b:function(){console.error(eval('this'));}}; a.b(); Object { b : function } var a = {b:function(){console.error((0,eval)('this'));}}; a.b(); Window { top : Window , window : Window , location : Location , external : Object , chrome : Object … }
這樣做的優點有兩個:速度快,更安全。 所以,你就能明白為什麼很多的架構喜歡用(0,eval)('this')來傳遞window對象的引用到命名空間閉包內。
架構封裝對外暴漏的介面 現在的Javascript代碼已經今非昔比,他可以運行在瀏覽器端,也可以運行在服務端,你可以模組化的方式進行開發,也可以以命名空間暴漏全域變數的方式進行開發。我們在封裝一個簡單的架構的時候都需要考慮到這所有的情況。於是,就出現了如下的常見的架構封裝方式:
(function (ROOT, factory) { if (typeof exports === 'object') { // Node. module.exports = factory(); } else if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(factory); } else { // Browser globals (root is window) ROOT.Decimal = factory(); }}((0,eval)(this), function () {});
基本滿足所有的需求。