问题重现
Javascript的数字类型只有一个number,没有短型、整型和浮点型、双浮点型等类型。由于Javascript在实现数字运算的时候,所采取的的浮点数类型实现方式,其会精确到小数点后16位。见下面的示例:
7*0.8 = 5.6000000000000005
0.1+0.2 = 0.3000000000000001
解决思路
要解决这个问题一般有两种方案,一种是用将数字转化为字符串来进行计算,另一种是将所有小数转化为整数进行计算后再将计算结果转化为对应的小数。我们主要采取第一种解决方案。
实现字符串相加、相乘的运算
实现字符串的相加相乘,有时候在某讯的面试题中会出现。下面我会完整实现,有可优化的地方可指出。
console.log(bigMut("567", "1234")); // 699678
function 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 () {});
基本满足所有的需求。