バベル(AST)で優雅に0.1+0.2を解決!=0.3の質問
10773 ワード
前言
0.1+0.2がどれくらいに等しいか知っていますか.0.1+0.7、0.8-0.2は?このような問題と同様に、外部ライブラリの導入や計算関数の定義にかかわらず、計算の代わりに関数を利用することが最終的な目的である多くの解決策が存在しています.例えば、下落率の上昇率の計算式:
の準備を
まず、なぜ
転送ゲート:JavaScript浮動小数点数計算精度の問題(例えば0.1+0.2!=0.3)を避ける方法
上の文章はとても详しくて、私は通俗的な言叶で概括します:私达の日常生活用の数字はすべて
十進法では
例えば10進法では1/3の表現が0.33333(無限ループ)であり、3進法では0.1を表す.3^-1が0.3333333であるからである.このような演算10進法では0.1が2進法では
babelについて
babelの動作原理は実際にはAST構文ツリーを用いた静的解析であり、例えば
babelは、テキストフォーマットのコードをこのようなjsonオブジェクトに翻訳することで、各異なる属性を遍歴し、再帰的に検索することができ、babelは、各行のコードが何をしているのかを知ることができます.babelプラグインの目的は、コードファイル全体の構文ツリーを再帰的に巡回し、変更する場所を見つけて対応する値に置き換え、コードを翻訳してブラウザに渡して実行することです.例えば、上記のコードの
オンライン翻訳AST転送ゲート
ASTノードタイプドキュメント転送ゲート
スタート
babelプラグインの開発プロセスbabel-plugin-handlebookについて
解決すべき問題:計算polyfillの作成 変更が必要なコードブロックを特定 現在のファイルを導入する必要があると判断したpolyfill(オンデマンド導入) polyfillの作成
polyfillは主に4つの関数を提供して、それぞれプラス、マイナス、乗算、除算の演算を置き換える必要があります.同時に、計算パラメータのデータ型を判断する必要があります.データ型がnumberでない場合は、元の計算方式を採用します.
accAdd
accSub
accMul
accDiv
原理:浮動小数点数を整数に変換して計算します.
位置決めコードブロック
babelプラグインの開発プロセスbabel-plugin-handlebookについて
babelのプラグインの導入方法は2つあります.通過.babelrcファイル導入プラグイン babel-loaderのoptions属性によるplugins の導入
babel-pluginは関数を受け入れ、関数はbabelパラメータを受信し、パラメータはbableの一般的な構造方法などの属性を含み、関数の戻り結果は以下のようなオブジェクトでなければならない.
visitorはASTの1つの遍歴ルックアップ器で、babelは深さでAST構文ツリーを優先的に遍歴しようとします.visitorの中の属性のkeyは操作が必要なASTノード名です.例えば、
関数パラメータpathには、現在のノードオブジェクトや、一般的なノード遍歴方法などのプロパティが含まれています.babel遍歴AST構文ツリーは深さ優先であり,あるサブリーフノード(ブランチの最終端)に遍歴器が遍歴すると祖先ノードに遡って遍歴操作を継続するため,各ノードは2回遍歴される.visitorの属性の値が関数である場合、この関数は最初にノードに入ったときに実行され、値がオブジェクトである場合、それぞれ2つの
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.
コードで置換する必要があるコードブロックは
したがって、この構文ツリーを構築してノードを置き換えるだけでいいので、babelは簡単な構築方法を提供し、
ASTは最終的に置換する必要がある文法ツリーですbabel.typesはノード作成方法の集合であり,各ノードの作成方法が含まれている.
最後に
導入の必要性を判断する方法
ノードループが完了した後、ファイルにいくつかのメソッドを導入する必要があることを知る必要があります.そのため、現在のファイルで使用されているメソッドをキャッシュする配列を定義し、ノードループがヒットしたときに要素を追加する必要があります.
AST遍歴が完了して最後に終了するノードは
完全なコードの例
Githubプロジェクトアドレス
使用方法:
プラグインを追加/.babelrc
または
/webpack.config.js
皆さんstarへようこそ⭐⭐⭐⭐⭐,何かアドバイスがあればissueを歓迎します.
リファレンスドキュメント
JavaScript浮動小数点数計算精度の問題を回避する方法(0.1+0.2!=0.3など)
AST explorer
@babel/types
babel-plugin-handlebook
0.1+0.2がどれくらいに等しいか知っていますか.0.1+0.7、0.8-0.2は?このような問題と同様に、外部ライブラリの導入や計算関数の定義にかかわらず、計算の代わりに関数を利用することが最終的な目的である多くの解決策が存在しています.例えば、下落率の上昇率の計算式:
( - )/ *100 + '%'
実際のコード:Mul(Div(Sub( , ), ), 100) + '%'
です.もともと分かりやすい四則演算の計算式は、コードの可読性があまり友好的ではなく、書くのも思考習慣に合わない.したがってbabelおよびAST構文ツリーを用いて,コード構築中に+ - * /
などの記号を書き換え,開発時に直接0.1+0.2
という形でコードを記述し,構築中にAdd(0.1, 0.2)
にコンパイルすることで,開発者が感知せずに計算ミスを解決し,コードの可読性を向上させる.の準備を
まず、なぜ
0.1+0.2
が0.3
に等しくないのかを理解します.転送ゲート:JavaScript浮動小数点数計算精度の問題(例えば0.1+0.2!=0.3)を避ける方法
上の文章はとても详しくて、私は通俗的な言叶で概括します:私达の日常生活用の数字はすべて
10
で、しかも10
は脳の思考のロジックに合って、コンピュータは2
のカウント方式を使っています.しかし、2つの異なる基数のカウントルールでは、すべての数が別のカウントルールの有限桁数の数に対応できるわけではありません(比較的言いにくいので、正確に記述されていないかもしれませんが、このような意味です).十進法では
0.1
は10^-1
つまり0.1を表し、バイナリでは0.1
は2^-1
つまり0.5を表す.例えば10進法では1/3の表現が0.33333(無限ループ)であり、3進法では0.1を表す.3^-1が0.3333333であるからである.このような演算10進法では0.1が2進法では
0.000110011......0011...... (0011 )
であるbabelについて
babelの動作原理は実際にはAST構文ツリーを用いた静的解析であり、例えば
let a = 100
babel処理前に翻訳された構文ツリー長のように:{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "NumericLiteral",
"extra": {
"rawValue": 100,
"raw": "100"
},
"value": 100
}
}
],
"kind": "let"
},
babelは、テキストフォーマットのコードをこのようなjsonオブジェクトに翻訳することで、各異なる属性を遍歴し、再帰的に検索することができ、babelは、各行のコードが何をしているのかを知ることができます.babelプラグインの目的は、コードファイル全体の構文ツリーを再帰的に巡回し、変更する場所を見つけて対応する値に置き換え、コードを翻訳してブラウザに渡して実行することです.例えば、上記のコードの
let
をvar
に変更すると、AST.kind = "var"
を実行するだけで、ASTは遍歴したオブジェクトになります.オンライン翻訳AST転送ゲート
ASTノードタイプドキュメント転送ゲート
スタート
babelプラグインの開発プロセスbabel-plugin-handlebookについて
解決すべき問題:
polyfillは主に4つの関数を提供して、それぞれプラス、マイナス、乗算、除算の演算を置き換える必要があります.同時に、計算パラメータのデータ型を判断する必要があります.データ型がnumberでない場合は、元の計算方式を採用します.
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);
}
原理:浮動小数点数を整数に変換して計算します.
位置決めコードブロック
babelプラグインの開発プロセスbabel-plugin-handlebookについて
babelのプラグインの導入方法は2つあります.
babel-pluginは関数を受け入れ、関数はbabelパラメータを受信し、パラメータはbableの一般的な構造方法などの属性を含み、関数の戻り結果は以下のようなオブジェクトでなければならない.
{
visitor: {
//...
}
}
visitorはASTの1つの遍歴ルックアップ器で、babelは深さでAST構文ツリーを優先的に遍歴しようとします.visitorの中の属性のkeyは操作が必要なASTノード名です.例えば、
VariableDeclaration
、BinaryExpression
などです.value値は関数またはオブジェクトとすることができます.完全な例は以下の通りです.{
visitor: {
VariableDeclaration(path){
//doSomething
},
BinaryExpression: {
enter(path){
//doSomething
}
exit(path){
//doSomething
}
}
}
}
関数パラメータpathには、現在のノードオブジェクトや、一般的なノード遍歴方法などのプロパティが含まれています.babel遍歴AST構文ツリーは深さ優先であり,あるサブリーフノード(ブランチの最終端)に遍歴器が遍歴すると祖先ノードに遡って遍歴操作を継続するため,各ノードは2回遍歴される.visitorの属性の値が関数である場合、この関数は最初にノードに入ったときに実行され、値がオブジェクトである場合、それぞれ2つの
enter
,exit
属性(オプション)が受信され、それぞれ遡及フェーズに入って実行される.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
のようなタイプであるため、このタイプのノードはBinaryExpression
であることがわかり、このタイプのノードをaccAdd(a, b)
に置換する必要がある.AST構文ツリーは以下の通りである.{
"type": "ExpressionStatement",
},
"expression": {
"type": "CallExpression",
},
"callee": {
"type": "Identifier",
"name": "accAdd"
},
"arguments": [
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
]
}
}
したがって、この構文ツリーを構築してノードを置き換えるだけでいいので、babelは簡単な構築方法を提供し、
babel.template
を利用して、あなたが望むノードを簡単に構築することができます.この関数は、コードプレースホルダとして大文字を使用したコード文字列パラメータを受信します.この関数は、コードプレースホルダを置換するためのパラメータとしてオブジェクトを受信する置換関数を返します.var preOperationAST = babel.template('FUN_NAME(ARGS)');
var AST = preOperationAST({
FUN_NAME: babel.types.identifier(replaceOperator), //
ARGS: [path.node.left, path.node.right] //
})
ASTは最終的に置換する必要がある文法ツリーですbabel.typesはノード作成方法の集合であり,各ノードの作成方法が含まれている.
最後に
path.replaceWith
を使用してノードを置換BinaryExpression: {
exit: function(path){
path.replaceWith(
preOperationAST({
FUN_NAME: t.identifier(replaceOperator),
ARGS: [path.node.left, path.node.right]
})
);
}
},
導入の必要性を判断する方法
ノードループが完了した後、ファイルにいくつかのメソッドを導入する必要があることを知る必要があります.そのため、現在のファイルで使用されているメソッドをキャッシュする配列を定義し、ノードループがヒットしたときに要素を追加する必要があります.
var needRequireCache = [];
...
return {
visitor: {
BinaryExpression: {
exit(path){
needRequireCache.push(path.node.operator)
// path.node.operator needRequireCache
...
}
}
}
}
...
AST遍歴が完了して最後に終了するノードは
Program
のexit
メソッドに違いないので、このメソッドではpolyfillを参照することができます.babel.template
構築ノードを使用して参照を挿入することもできます.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
の役割は、現在の構文ツリーにノードを挿入することです.したがって、最後の効果はこのようになります.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プロジェクトアドレス
使用方法:
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を歓迎します.
リファレンスドキュメント
JavaScript浮動小数点数計算精度の問題を回避する方法(0.1+0.2!=0.3など)
AST explorer
@babel/types
babel-plugin-handlebook