本篇文章主要是使用 NestJs + Sequelize + MySQL 完成基礎運行, 帶大家瞭解 Node 服務端的基礎搭建,也可以順便看看 Java SpringBoot 項目的基礎結構,它倆真的非常相似,不信你去問服務端開發同學。 ...
來源:https://zhuanlan.zhihu.com/p/191395766
問題描述
在 JavaScript 中整數和浮點數都屬於 Number
數據類型,所有數字都是以 64 位浮點數形式儲存,即便整數也是如此。 所以我們在列印 1.00
這樣的浮點數的結果是 1
而非 1.00
。在一些特殊的數值表示中,例如金額,這樣看上去有點變扭,但是至少值是正確了。然而要命的是,當浮點數做數學運算的時候,你經常會發現一些問題,舉幾個例子:
JavaScript 代碼:
// 加法 =====================
// 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
都會被轉換成二進位,但由於浮點數用二進位表達時是無窮的,例如。
JavaScript 代碼:
0.1 -> 0.0001100110011001...(無限)
0.2 -> 0.0011001100110011...(無限)
IEEE 754 標準的 64 位雙精度浮點數的小數部分最多支持 53 位二進位位,所以兩者相加之後得到二進位為:
JavaScript 代碼:
0.0100110011001100110011001100110011001100110011001100
因浮點數小數位的限制而截斷的二進位數字,再轉換為十進位,就成了 0.30000000000000004
。所以在進行算術計算時會產生誤差。
整數的精度問題
在 Javascript 中,整數精度同樣存在問題,先來看看問題:
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/
這幾個類庫幫我們解決很多這類問題,不過通常我們前端做這類運算通常只用於表現層,應用並不是很多。所以很多時候,一個函數能解決的問題不需要引用一個類庫來解決。
下麵介紹各個更加簡單的解決方案。
整數表示
對於整數,我們可以通過用String
類型的表示來取值或傳值,否則會喪失精度。
格式化數字、金額、保留幾位小數等
如果只是格式化數字、金額、保留幾位小數等可以查看這裡 https://www.html.cn/archives/7324
浮點數運算
toFixed() 方法
浮點數運算的解決方案有很多,這裡給出一種目前常用的解決方案, 在判斷浮點數運算結果前對計算結果進行精度縮小,因為在精度縮小的過程總會自動四捨五入。
toFixed() 方法使用定點表示法來格式化一個數,會對結果進行四捨五入。語法為:
JavaScript 代碼:
numObj.toFixed(digits)
參數 digits
表示小數點後數字的個數;介於 0 到 20 (包括)之間,實現環境可能支持更大範圍。如果忽略該參數,則預設為 0。
返回一個數值的字元串表現形式,不使用指數記數法,而是在小數點後有 digits
位數字。該數值在必要時進行四捨五入,另外在必要時會用 0 來填充小數部分,以便小數部分有指定的位數。 如果數值大於 1e+21
,該方法會簡單調用 Number.prototype.toString()
並返回一個指數記數法格式的字元串。
特別註意:toFixed() 返回一個數值的字元串表現形式。
具體可以查看 MDN中的說明,那麼我們可以這樣解決精度問題:
JavaScript 代碼:
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 代碼:
/**
** 加法函數,用來得到精確的加法結果
** 說明: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 代碼:
/**
** 減法函數,用來得到精確的減法結果
** 說明: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 代碼:
/**
** 乘法函數,用來得到精確的乘法結果
** 說明: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 代碼:
/**
** 除法函數,用來得到精確的除法結果
** 說明: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);
};
相關推薦