官术网_书友最值得收藏!

1.2.4 浮點型運算

在JavaScript中,整數和浮點數都屬于Number類型,它們都統一采用64位浮點數進行存儲。

雖然它們存儲數據的方式是一致的,但是在進行數值運算時,卻會表現出明顯的差異性。整數參與運算時,得到的結果往往會和我們所想的一樣,例如下面的代碼。


// 加法
1 + 2 = 3
7 + 1 = 8

// 減法
15 - 12 = 3
3 - 2 = 1

// 乘法
7 * 180 = 1260
97 * 100 = 9700

// 除法
3 / 1 = 3
69 / 10 = 6.9

而對于浮點型運算,有時卻會出現一些意想不到的結果,如下面的代碼所示。


// 加法
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999

// 減法
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998

// 乘法
0.7 * 180 = 125.99999999999999
9.7 * 100 = 969.9999999999999

// 除法
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999

得到這樣的結果,大家是不是覺得很奇怪呢?0.1 + 0.2為什么不是等于0.3,而是等于0.30000000000000004呢?接下來我們一探究竟。

1. 問題原因

首先我們來看看一個浮點型數在計算機中的表示,它總共長度是64位,其中最高位為符號位,接下來的11位為指數位,最后的52位為小數位,即有效數字的部分。

· 第0位:符號位sign表示數的正負,0表示正數,1表示負數。

· 第1位到第11位:存儲指數部分,用e表示。

· 第12位到第63位:存儲小數部分(即有效數字),用f表示,如圖1-1所示。

圖1-1

因為浮點型數使用64位存儲時,最多只能存儲52位的小數位,對于一些存在無限循環的小數位浮點數,會截取前52位,從而丟失精度,所以會出現上面實例中的結果。

2. 計算過程

接下來以0.1 + 0.2 = 0.30000000000000004的運算為例,看看為什么會得到這個計算結果。

首先將各個浮點數的小數位按照“乘2取整,順序排列”的方法轉換成二進制表示。

具體做法是用2乘以十進制小數,得到積,將積的整數部分取出;然后再用2乘以余下的小數部分,又得到一個積;再將積的整數部分取出,如此推進,直到積中的小數部分為零為止。

然后把取出的整數部分按順序排列起來,先取的整數作為二進制小數的高位有效位,后取的整數作為低位有效位,得到最終結果。

0.1轉換為二進制表示的計算過程如下。


0.1 * 2 = 0.2 //取出整數部分0

0.2 * 2 = 0.4 //取出整數部分0

0.4 * 2 = 0.8 //取出整數部分0

0.8 * 2 = 1.6 //取出整數部分1

0.6 * 2 = 1.2 //取出整數部分1

0.2 * 2 = 0.4 //取出整數部分0

0.4 * 2 = 0.8 //取出整數部分0

0.8 * 2 = 1.6 //取出整數部分1

0.6 * 2 = 1.2 //取出整數部分1

1.2取出整數部分1后,剩余小數為0.2,與這一輪運算的第一位相同,表示這將是一個無限循環的計算過程。


0.2 * 2 = 0.4 //取出整數部分0

0.4 * 2 = 0.8 //取出整數部分0

0.8 * 2 = 1.6 //取出整數部分1

0.6 * 2 = 1.2 //取出整數部分1
...

因此0.1轉換成二進制表示為0.0 0011 0011 0011 0011 0011 0011……(無限循環)。

同理對0.2進行二進制的轉換,計算過程與上面類似,直接從0.2開始,相比于0.1,少了第一位的0,其余位數完全相同,結果為0.0011 0011 0011 0011 0011 0011……(無限循環)。

將0.1與0.2相加,然后轉換成52位精度的浮點型表示。


 0.0001 1001 1001 1001 1001 1001  1001 1001 1001 1001 1001 1001 1001   (0.1)
+ 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011   (0.2)
= 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100

得到的結果為0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100,轉換成十進制值為0.30000000000000004。

3. 解決方案

通過上面詳細的講解,相信大家已經了解了對浮點數進行運算時會存在的問題,那么我們該如何解決呢?

這里提供一種方法,主要思路是將浮點數先乘以一定的數值轉換為整數,通過整數進行運算,然后將結果除以相同的數值轉換成浮點數后返回。

下面提供一套用于做浮點數加減乘除運算的代碼。


const operationObj = {
   /**
    * 處理傳入的參數,不管傳入的是數組還是以逗號分隔的參數都處理為數組
    * @param args
    * @returns {*}
    */
   getParam(args) {
      return Array.prototype.concat.apply([], args);
   },

   /**
    * 獲取每個數的乘數因子,根據小數位數計算
    * 1.首先判斷是否有小數點,如果沒有,則返回1;
    * 2.有小數點時,將小數位數的長度作為Math.pow()函數的參數進行計算
    * 例如2的乘數因子為1,2.01的乘數因子為100
    * @param x
    * @returns {number}
    */
   multiplier(x) {
      let parts = x.toString().split('.');
      return parts.length < 2 ? 1 : Math.pow(10, parts[1].length);
   },

   /**
    * 獲取多個數據中最大的乘數因子
    * 例如1.3的乘數因子為10,2.13的乘數因子為100
    * 則1.3和2.13的最大乘數因子為100
    * @returns {*}
    */
   correctionFactor() {
       let args = Array.prototype.slice.call(arguments);
       let argArr = this.getParam(args);
       return argArr.reduce((accum, next) => {
           let num = this.multiplier(next);
           return Math.max(accum, num);
       }, 1);
   },

   /**
    * 加法運算
    * @param args
    * @returns {number}
    */
   add(...args) {
       let calArr = this.getParam(args);
       // 獲取參與運算值的最大乘數因子
       let corrFactor = this.correctionFactor(calArr);
       let sum = calArr.reduce((accum, curr) => {
           // 將浮點數乘以最大乘數因子,轉換為整數參與運算
           return accum + Math.round(curr * corrFactor);
       }, 0);
       // 除以最大乘數因子
       return sum / corrFactor;
   },

   /**
    * 減法運算
    * @param args
    * @returns {number}
    */
   subtract(...args) {
       let calArr = this.getParam(args);
       let corrFactor = this.correctionFactor(calArr);
       let di? = calArr.reduce((accum, curr, curIndex) => {
          // reduce()函數在未傳入初始值時,curIndex從1開始,第一位參與運算的值需要
          // 乘以最大乘數因子
          if (curIndex === 1) {
              return Math.round(accum * corrFactor) - Math.round(curr * corrFactor);
          }
          // accum作為上一次運算的結果,就無須再乘以最大因子
          return Math.round(accum) - Math.round(curr * corrFactor);
       });
     // 除以最大乘數因子
       return di? / corrFactor;
   },

   /**
    * 乘法運算
    * @param args
    * @returns {*}
    */
   multiply(...args) {
      let calArr = this.getParam(args);
      let corrFactor = this.correctionFactor(calArr);
      calArr = calArr.map((item) => {
          // 乘以最大乘數因子
          return item * corrFactor;
      });
      let multi = calArr.reduce((accum, curr) => {
          return Math.round(accum) * Math.round(curr);
      }, 1);
      // 除以最大乘數因子
      return multi / Math.pow(corrFactor, calArr.length);
   },

   /**
    * 除法運算
    * @param args
    * @returns {*}
    */
   divide(...args) {
       let calArr = this.getParam(args);
       let quotient = calArr.reduce((accum, curr) => {
           let corrFactor = this.correctionFactor(accum, curr);
           // 同時轉換為整數參與運算
           return Math.round(accum * corrFactor) / Math.round(curr * corrFactor);
       });
       return quotient;
   }
};

接下來我們通過以下這些代碼對加減乘除運算分別做測試,運算結果和我們期望的一致。


console.log(operationObj.add(0.1, 0.7));      // 0.8
console.log(operationObj.subtract(0.3, 0.2)); // 0.1
console.log(operationObj.multiply(0.7, 180)); // 126
console.log(operationObj.divide(0.3, 0.1));   // 3

主站蜘蛛池模板: 湖北省| 锡林浩特市| 尉犁县| 中江县| 镇沅| 手机| 桂林市| 哈尔滨市| 竹山县| 寿宁县| 文登市| 凤山县| 来凤县| 烟台市| 长岭县| 两当县| 上思县| 库尔勒市| 五大连池市| 博乐市| 温宿县| 宜阳县| 穆棱市| 三门峡市| 理塘县| 禹城市| 长海县| 天镇县| 桑植县| 富顺县| 玉环县| 中方县| 汕尾市| 井研县| 海南省| 德令哈市| 荔浦县| 东乡县| 油尖旺区| 宁明县| 博白县|