亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

利用babel(AST)優(yōu)雅地解決0.1+0.2!=0.3的問(wèn)題

張巨偉 / 3650人閱讀

摘要:因此利用以及語(yǔ)法樹(shù)在代碼構(gòu)建過(guò)程中重寫(xiě)等符號(hào),開(kāi)發(fā)時(shí)直接以這樣的形式編寫(xiě)代碼,在構(gòu)建過(guò)程中編譯成,從而在開(kāi)發(fā)人員無(wú)感知的情況下解決計(jì)算失精的問(wèn)題,提升代碼的可讀性。

前言

你了解過(guò)0.1+0.2到底等于多少嗎?那0.1+0.7,0.8-0.2呢?
類似于這種問(wèn)題現(xiàn)在已經(jīng)有了很多的解決方案,無(wú)論引入外部庫(kù)或者是自己定義計(jì)算函數(shù)最終的目的都是利用函數(shù)去代替計(jì)算。例如一個(gè)漲跌幅百分比的一個(gè)計(jì)算公式:(現(xiàn)價(jià)-原價(jià))/原價(jià)*100 + "%"實(shí)際代碼:Mul(Div(Sub(現(xiàn)價(jià), 原價(jià)), 原價(jià)), 100) + "%"。原本一個(gè)很易懂的四則運(yùn)算的計(jì)算公式在代碼里面的可讀性變得不太友好,編寫(xiě)起來(lái)也不太符合思考習(xí)慣。
因此利用babel以及AST語(yǔ)法樹(shù)在代碼構(gòu)建過(guò)程中重寫(xiě)+ - * /等符號(hào),開(kāi)發(fā)時(shí)直接以0.1+0.2這樣的形式編寫(xiě)代碼,在構(gòu)建過(guò)程中編譯成Add(0.1, 0.2),從而在開(kāi)發(fā)人員無(wú)感知的情況下解決計(jì)算失精的問(wèn)題,提升代碼的可讀性。

準(zhǔn)備

首先了解一下為什么會(huì)出現(xiàn)0.1+0.2不等于0.3的情況:

傳送門(mén):如何避開(kāi)JavaScript浮點(diǎn)數(shù)計(jì)算精度問(wèn)題(如0.1+0.2!==0.3)

上面的文章講的很詳細(xì)了,我用通俗點(diǎn)的語(yǔ)言概括一下:
我們?nèi)粘I钣玫臄?shù)字都是10進(jìn)制的,并且10進(jìn)制符合大腦思考邏輯,而計(jì)算機(jī)使用的是2進(jìn)制的計(jì)數(shù)方式。但是在兩個(gè)不同基數(shù)的計(jì)數(shù)規(guī)則中,其中并不是所有的數(shù)都能對(duì)應(yīng)另外一個(gè)計(jì)數(shù)規(guī)則里有限位數(shù)的數(shù)(比較拗口,可能描述的不太準(zhǔn)確,但是意思就是這個(gè)樣子)。

在十進(jìn)制中的0.1表示是10^-1也就是0.1,在二進(jìn)制中的0.1表示是2^-1也就是0.5。

例如在十進(jìn)制中1/3的表現(xiàn)方式為0.33333(無(wú)限循環(huán)),而在3進(jìn)制中的表示為0.1,因?yàn)?^-1就是0.3333333……
按照這種運(yùn)算十進(jìn)制中的0.1在二進(jìn)制的表示方式為0.000110011......0011...... (0011無(wú)限循環(huán))

了解babel

babel的工作原理實(shí)際上就是利用AST語(yǔ)法樹(shù)來(lái)做的靜態(tài)分析,例如let a = 100在babel處理之前翻譯成的語(yǔ)法樹(shù)長(zhǎng)這樣:

{
    "type": "VariableDeclaration",
    "declarations": [
      {
        "type": "VariableDeclarator",
        "id": {
          "type": "Identifier",
          "name": "a"
        },
        "init": {
          "type": "NumericLiteral",
          "extra": {
            "rawValue": 100,
            "raw": "100"
          },
          "value": 100
        }
      }
    ],
    "kind": "let"
  },

babel把一個(gè)文本格式的代碼翻譯成這樣的一個(gè)json對(duì)象從而能夠通過(guò)遍歷和遞歸查找每個(gè)不同的屬性,通過(guò)這樣的手段babel就能知道每一行代碼到底做了什么。而babel插件的目的就是通過(guò)遞歸遍歷整個(gè)代碼文件的語(yǔ)法樹(shù),找到需要修改的位置并替換成相應(yīng)的值,然后再翻譯回代碼交由瀏覽器去執(zhí)行。例如我們把上面的代碼中的let改成var我們只需要執(zhí)行AST.kind = "var",AST為遍歷得到的對(duì)象。

在線翻譯AST傳送門(mén)  
AST節(jié)點(diǎn)類型文檔傳送門(mén)
開(kāi)始
了解babel插件的開(kāi)發(fā)流程 babel-plugin-handlebook

我們需要解決的問(wèn)題:

計(jì)算polyfill的編寫(xiě)

定位需要更改的代碼塊

判斷當(dāng)前文件需要引入的polyfill(按需引入)

polyfill的編寫(xiě)

polyfill主要需要提供四個(gè)函數(shù)分別用于替換加、減、乘、除的運(yùn)算,同時(shí)還需要判斷計(jì)算參數(shù)數(shù)據(jù)類型,如果數(shù)據(jù)類型不是number則采用原本的計(jì)算方式:

accAdd

function accAdd(arg1, arg2) {
    if(typeof arg1 !== "number" || typeof arg2 !== "number"){
        return 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;
}

accSub

function accSub(arg1, arg2) {
    if(typeof arg1 !== "number" || typeof arg2 !== "number"){
        return 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)); 
    n = (r1 >= r2) ? r1 : r2;
    return Number(((arg1 * m - arg2 * m) / m).toFixed(n));
}

accMul

function accMul(arg1, arg2) {
    if(typeof arg1 !== "number" || typeof arg2 !== "number"){
        return 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);
}

accDiv

function accDiv(arg1, arg2) {
    if(typeof arg1 !== "number" || typeof arg2 !== "number"){
        return 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) {
    }
    r1 = Number(arg1.toString().replace(".", ""));
    r2 = Number(arg2.toString().replace(".", ""));
    return (r1 / r2) * Math.pow(10, t2 - t1);
}

原理:將浮點(diǎn)數(shù)轉(zhuǎn)換為整數(shù)來(lái)進(jìn)行計(jì)算。

定位代碼塊
了解babel插件的開(kāi)發(fā)流程 babel-plugin-handlebook

babel的插件引入方式有兩種:

通過(guò).babelrc文件引入插件

通過(guò)babel-loader的options屬性引入plugins

babel-plugin接受一個(gè)函數(shù),函數(shù)接收一個(gè)babel參數(shù),參數(shù)包含bable常用構(gòu)造方法等屬性,函數(shù)的返回結(jié)果必須是以下這樣的對(duì)象:

{
    visitor: {
        //...
    }
}

visitor是一個(gè)AST的一個(gè)遍歷查找器,babel會(huì)嘗試以深度優(yōu)先遍歷AST語(yǔ)法樹(shù),visitor里面的屬性的key為需要操作的AST節(jié)點(diǎn)名如VariableDeclarationBinaryExpression等,value值可為一個(gè)函數(shù)或者對(duì)象,完整示例如下:

{
    visitor: {
        VariableDeclaration(path){
            //doSomething
        },
        BinaryExpression: {
            enter(path){
                //doSomething
            }
            exit(path){
                //doSomething
            }
        }
    }
}

函數(shù)參數(shù)path包含了當(dāng)前節(jié)點(diǎn)對(duì)象,以及常用節(jié)點(diǎn)遍歷方法等屬性。
babel遍歷AST語(yǔ)法樹(shù)是以深度優(yōu)先,當(dāng)遍歷器遍歷至某一個(gè)子葉節(jié)點(diǎn)(分支的最終端)的時(shí)候會(huì)進(jìn)行回溯到祖先節(jié)點(diǎn)繼續(xù)進(jìn)行遍歷操作,因此每個(gè)節(jié)點(diǎn)會(huì)被遍歷到2次。當(dāng)visitor的屬性的值為函數(shù)的時(shí)候,該函數(shù)會(huì)在第一次進(jìn)入該節(jié)點(diǎn)的時(shí)候執(zhí)行,當(dāng)值為對(duì)象的時(shí)候分別接收兩個(gè)enterexit屬性(可選),分別在進(jìn)入與回溯階段執(zhí)行。

As we traverse down each branch of the tree we eventually hit dead ends where we need to traverse back up the tree to get to the next node. Going down the tree we enter each node, then going back up we exit each node.

在代碼中需要被替換的代碼塊為a + b這樣的類型,因此我們得知該類型的節(jié)點(diǎn)為BinaryExpression,而我們需要把這個(gè)類型的節(jié)點(diǎn)替換成accAdd(a, b),AST語(yǔ)法樹(shù)如下:

{
        "type": "ExpressionStatement",
        },
        "expression": {
          "type": "CallExpression",
          },
          "callee": {
            "type": "Identifier",
            "name": "accAdd"
          },
          "arguments": [
            {
              "type": "Identifier",
              "name": "a"
            },
            {
              "type": "Identifier",
              "name": "b"
            }
          ]
        }
      }

因此只需要將這個(gè)語(yǔ)法樹(shù)構(gòu)建出來(lái)并替換節(jié)點(diǎn)就行了,babel提供了簡(jiǎn)便的構(gòu)建方法,利用babel.template可以方便的構(gòu)建出你想要的任何節(jié)點(diǎn)。這個(gè)函數(shù)接收一個(gè)代碼字符串參數(shù),代碼字符串中采用大寫(xiě)字符作為代碼占位符,該函數(shù)返回一個(gè)替換函數(shù),接收一個(gè)對(duì)象作為參數(shù)用于替換代碼占位符。

var preOperationAST = babel.template("FUN_NAME(ARGS)");
var AST = preOperationAST({
    FUN_NAME: babel.types.identifier(replaceOperator), //方法名
    ARGS: [path.node.left, path.node.right] //參數(shù)
})

AST就是最終需要替換的語(yǔ)法樹(shù),babel.types是一個(gè)節(jié)點(diǎn)創(chuàng)建方法的集合,里面包含了各個(gè)節(jié)點(diǎn)的創(chuàng)建方法。

最后利用path.replaceWith替換節(jié)點(diǎn)

BinaryExpression: {
    exit: function(path){
        path.replaceWith(
            preOperationAST({
                FUN_NAME: t.identifier(replaceOperator),
                ARGS: [path.node.left, path.node.right]
            })
        );
    }
},
判斷需要引入的方法

在節(jié)點(diǎn)遍歷完畢之后,我需要知道該文件一共需要引入幾個(gè)方法,因此需要定義一個(gè)數(shù)組來(lái)緩存當(dāng)前文件使用到的方法,在節(jié)點(diǎn)遍歷命中的時(shí)候向里面添加元素。

var needRequireCache = [];
...
    return {
        visitor: {
            BinaryExpression: {
                exit(path){
                    needRequireCache.push(path.node.operator)
                    //根據(jù)path.node.operator判斷向needRequireCache添加元素
                    ...
                }
            }
        }
    }
...

AST遍歷完畢最后退出的節(jié)點(diǎn)肯定是Programexit方法,因此可以在這個(gè)方法里面對(duì)polyfill進(jìn)行引用。
同樣也可以利用babel.template構(gòu)建節(jié)點(diǎn)插入引用:

var requireAST = template("var PROPERTIES = require(SOURCE)");
...
    function preObjectExpressionAST(keys){
        var properties = keys.map(function(key){
            return babel.types.objectProperty(t.identifier(key),t.identifier(key), false, true);
        });
        return t.ObjectPattern(properties);
    }
...
    Program: {
        exit: function(path){
            path.unshiftContainer("body", requireAST({
                PROPERTIES: preObjectExpressionAST(needRequireCache),
                SOURCE: t.stringLiteral("babel-plugin-arithmetic/src/calc.js")
            }));
            needRequireCache = [];
        }
    },
...

path.unshiftContainer的作用就是在當(dāng)前語(yǔ)法樹(shù)插入節(jié)點(diǎn),所以最后的效果就是這個(gè)樣子:

var a = 0.1 + 0.2;
//0.30000000000000004
    ↓ ↓ ↓ ↓ ↓ ↓
var { accAdd } = require("babel-plugin-arithmetic/src/calc.js");
var a = accAdd(0.1, 0.2);
//0.3
var a = 0.1 + 0.2;
var b = 0.8 - 0.2;
//0.30000000000000004
//0.6000000000000001
    ↓ ↓ ↓ ↓ ↓ ↓
var { accAdd, accSub } = require("babel-plugin-arithmetic/src/calc.js");
var a = accAdd(0.1, 0.2);
var a = accSub(0.8, 0.2);
//0.3
//0.6
完整代碼示例
Github項(xiàng)目地址

使用方法:

npm install babel-plugin-arithmetic --save-dev

添加插件
/.babelrc

{
    "plugins": ["arithmetic"]
}

或者

/webpack.config.js

...
{
    test: /.js$/,
    loader: "babel-loader",
    option: {
        plugins: [
            require("babel-plugin-arithmetic")
        ]
    },
},
...

歡迎各位小伙伴給我star?????,有什么建議歡迎issue我。

參考文檔
如何避開(kāi)JavaScript浮點(diǎn)數(shù)計(jì)算精度問(wèn)題(如0.1+0.2!==0.3)   
AST explorer
@babel/types
babel-plugin-handlebook

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/104168.html

相關(guān)文章

  • 為什么0.1+0.2不等于0.3

    摘要:又如,對(duì)于,結(jié)果其實(shí)并不是,但是最接近真實(shí)結(jié)果的數(shù),比其它任何浮點(diǎn)數(shù)都更接近。許多語(yǔ)言也就直接顯示結(jié)果為了,而不展示一個(gè)浮點(diǎn)數(shù)的真實(shí)結(jié)果了。小結(jié)本文主要介紹了浮點(diǎn)數(shù)計(jì)算問(wèn)題,簡(jiǎn)單回答了為什么以及怎么辦兩個(gè)問(wèn)題為什么不等于。 原文地址:為什么0.1+0.2不等于0.3 先看兩個(gè)簡(jiǎn)單但詭異的代碼: 0.1 + 0.2 > 0.3 // true 0.1 * 0.1 = 0.01000000...

    Profeel 評(píng)論0 收藏0
  • 如何解決0.1 +0.2===0.30000000000000004類問(wèn)題

    摘要:方法使用定點(diǎn)表示法來(lái)格式化一個(gè)數(shù),會(huì)對(duì)結(jié)果進(jìn)行四舍五入。該數(shù)值在必要時(shí)進(jìn)行四舍五入,另外在必要時(shí)會(huì)用來(lái)填充小數(shù)部分,以便小數(shù)部分有指定的位數(shù)。如果數(shù)值大于,該方法會(huì)簡(jiǎn)單調(diào)用并返回一個(gè)指數(shù)記數(shù)法格式的字符串。在環(huán)境中,只能是之間,測(cè)試版本為。 showImg(https://segmentfault.com/img/remote/1460000011913134?w=768&h=521)...

    yuanzhanghu 評(píng)論0 收藏0
  • JS數(shù)值

    摘要:由于浮點(diǎn)數(shù)不是精確的值,所以涉及小數(shù)的比較和運(yùn)算要特別小心。根據(jù)標(biāo)準(zhǔn),位浮點(diǎn)數(shù)的指數(shù)部分的長(zhǎng)度是個(gè)二進(jìn)制位,意味著指數(shù)部分的最大值是的次方減。也就是說(shuō),位浮點(diǎn)數(shù)的指數(shù)部分的值最大為。 一 前言 這篇文章主要解決以下三個(gè)問(wèn)題: 問(wèn)題1:浮點(diǎn)數(shù)計(jì)算精確度的問(wèn)題 0.1 + 0.2; //0.30000000000000004 0.1 + 0.2 === 0.3; // ...

    williamwen1986 評(píng)論0 收藏0
  • JavaScript浮點(diǎn)運(yùn)算0.2+0.1 !== 0.3

    摘要:標(biāo)準(zhǔn)二進(jìn)制浮點(diǎn)數(shù)算法就是一個(gè)對(duì)實(shí)數(shù)進(jìn)行計(jì)算機(jī)編碼的標(biāo)準(zhǔn)。然后把取出的整數(shù)部分按順序排列起來(lái),先取的整數(shù)作為二進(jìn)制小數(shù)的高位有效位,后取的整數(shù)作為低位有效位。 浮點(diǎn)運(yùn)算JavaScript 本文主要討論JavaScript的浮點(diǎn)運(yùn)算,主要包括 JavaScript number基本類型 二進(jìn)制表示十進(jìn)制 浮點(diǎn)數(shù)的精度 number 數(shù)字類型 在JavaScript中,數(shù)字只有numb...

    iflove 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<