本文講解的是怎麼實現一個工具庫並打包發佈到npm給你們使用。本文實現的工具是一個分數計算器,你們考慮以下狀況:javascript
這是一個分數計算式,使用JS原生也是能夠計算的,可是隻能獲得一個近視值:java
Math.sqrt(Math.pow(((1/3+3.5)*2/9-27/109)/(889/654),4)); // 0.1975308641975308
由於上面好幾個分數都除不盡,因此JS計算只能算出一個近似值,若是咱們須要一個精確值,就須要用分數來表示,JS原生是不支持分數計算的,本文實現的工具庫就能夠進行這種分數計算,使用本文的庫計算以下:node
fc('1/3') .plus(3.5) .times('2/9') .minus('27/109') .div('889/654') .pow(4) .sqrt() .toFraction(); // 輸出: 16/81
用咱們的庫輸出的就是一個精確的分數,本庫還能夠將這個分數轉化爲精確的循環小數,好比上面的分數轉化成循環小數就是:webpack
fc('16/81').toRecurringDecimal(); // "0.(197530864)"
上面計算的輸出是:0.(197530864)
。其中()
裏面的是循環的數字,也就是說原來的小數是0.197530864197530864197530864...
。本工具還能夠將循環小數轉換回來:git
fc('0.(197530864)').toFraction(); // 16/81
由於本工具實質上都是在進行分數計算,分子和分母都是整數,因此JS自己浮點數計算不許的問題本工具也解決了:github
0.1 + 0.2; // 0.30000000000000004 fc(0.1).plus(0.2).toNumber(); // 0.3
這個庫的名字是fraction-calculator
,已經發布到npm,你們能夠安裝試用:web
npm install fraction-calculator --save
本工具(如下簡稱fc
)代碼使用GitHub託管,歡迎你們star
,有任何問題能夠直接在GitHub提issue。算法
GitHub地址: https://github.com/dennis-jiang/fraction-calculatornpm
GitHub上有詳細的使用說明,本文接下來的篇幅會詳細講解怎麼實現功能和打包發佈。json
先來看看咱們須要實現的API,內心大概有個數
從上圖能夠看出,咱們的API主要分以下幾類:
下面咱們分別來說講每部分怎麼實現:
由於咱們進行的是分數計算,JS沒有分數數據類型,咱們須要一個字符串來表示分數,並且在數學中,一個大於1的分數,好比\(\frac{5}{2}\)既能夠表示爲這種形式,也能夠表示爲\(2\frac{1}{2}\),這種讀做「二又二分之一」,咱們這兩種字符串都須要支持。爲了方便使用,用戶直接用數字確定也是要支持的。還有前面說過,咱們支持循環小數轉分數,因此循環小數也要支持,我這裏支持兩種循環小數的表示方法,使用''
和()
來標記循環部分均可以。爲了讓用戶使用更方便,最好new
關鍵字也省了,像jQuery那樣,直接拿來就用。爲了讓咱們的庫變得更穩健,咱們最好也支持傳入本身的一個實例,就能夠隨便嵌套了,好比fc(fc(0.5).plus('1/3')).times(5)
。最後,順便也支持下兩個參數吧,萬一有用戶喜歡呢,第一個參數表示分子,第二個表示分母。總結下來,咱們的構造器的需求是:
做爲項目的第一步,確定是要想一想個人API要以什麼形式組織,以什麼形式暴露出去。這就讓我想起了jQuery,n年前我還在用jQuery作網頁,一個$直接拿來點點點就好了,想要啥就點啥。作fc的時候就想着能不能也讓用戶用的這麼爽,直接用fc點點點就行,因而就借鑑了jQuery的作法,不用new就能夠直接調用。關於jQuery架構的詳細解釋能夠看這篇文章。下面咱們直接上成品:
// 首先建立一個fc的函數,咱們最終要返回的其實就是一個fc的實例 // 可是咱們又不想讓用戶new,那麼麻煩 // 因此咱們要在構造函數裏面給他new好這個實例,直接返回 function FractionCalculator(numStr, denominator) { // 咱們new的實際上是fc.fn.init return new FractionCalculator.fn.init(numStr, denominator); } // fc.fn其實就是fc的原型,算是個簡寫,全部實例都會擁有這上面的方法 FractionCalculator.fn = FractionCalculator.prototype = {}; // 這個其實才是真正的構造函數,這個構造函數也很簡單,就是將傳入的參數轉化爲分數 // 而後將轉化的分數掛載到this上,這裏的this其實就是返回的實例 FractionCalculator.fn.init = function(numStr, denominator) { this.fraction = FractionCalculator.getFraction(numStr, denominator); }; // 前面new的是init,其實返回的是init的實例 // 爲了讓返回的實例可以訪問到fc的方法,將init的原型指向fc的原型 FractionCalculator.fn.init.prototype = FractionCalculator.fn;
上面代碼其實就完成了咱們的基礎架構,裏面用到了JS面向對象的知識,若是對JS面向對象不是很瞭解,能夠看看這篇文章。若是對上面代碼有點迷糊,強烈建議看看前面連接的兩篇文章,所謂學以至用,就是要先學理論而後纔拿來用嘛。
有了上面的基礎架構,咱們要添加實例方法和靜態方法就很簡單了:
// 添加實例方法這樣寫,下面是plus方法,注意這裏是在fn上,也就是原型上 FractionCalculator.fn.plus = function() {} // 添加靜態方法這樣寫,下面是gcd方法,注意這裏沒在fn上 FractionCalculator.gcd = function() {}
前面咱們在init方法裏面其實將計算好的分數掛載到了this.fraction
上,這裏的fraction
結構其實很簡單,就一個分子和分母。後面咱們全部的操做其實都在玩這個對象:
let fraction = { numerator, // 分子 denominator, // 分母 };
前面說了,JS自己對浮點數計算並不許,fc可以解決這個問題,解決這個問題的方法就是當構造器接收到浮點數時,將它轉換爲整數的分子和分母。可能有朋友據說過JS將浮點數轉換成整數直接乘以10的n次方就行,n是小數位數,算完了再除以這個數就行。我最開始也是這麼實現的,直到我遇到了它:0.1478
。0.1478
並非一個什麼特殊的數字,就是我測試的時候隨便輸的一個數,按照這個思路,應該將它乘以10000,而後它就會變成整數1478吧,咱們來看看結果:
結果有點出乎意料啊,看來這條路走不通了。最終個人方案是做爲字符串處理,先將數字轉換爲字符串,把小數點去掉,而後再轉換成數字,這樣就能獲得正確的數字了。小數全程不參與運算。
而後咱們構造器還要支持兩個數字,帶整數的字符串和不帶整數的字符串,這些都不難直接將拿到的參數解析成分子和分母塞到這個對象上就好了。另外咱們要支持另外一個實例做爲參數,那就用instanceof
檢查下傳入參數是否是fc的實例,若是是就將傳入參數的fraction
掛載到當前實例就好了。這兩部分代碼都不難,有興趣的朋友能夠去GitHub看我源碼。真正有點麻煩的是循環小數轉分數。
作這個需求的時候,個人數學知識報警了,雖然是中學知識,可是這麼多年沒用,仍是忘記了,趕忙回去翻翻課本才搞定。下面一塊兒來複習下中學數學知識:循環小數轉分數。
題目:請將循環小數5.45(689)轉換成分數,其中括號裏面的是循環部分。
解這個題以前先來複習一個概念,循環小數分爲純循環小數和混循環小數兩種:
純循環小數:小數部分所有循環,好比0.(689)
混循環小數:小數部分前面有幾位不參與循環,後面的纔是循環部分,好比0.234(689)
再來複習一個定理:
任何純循環小數均可以轉換爲,分母爲n個9的分數,n爲循環小數的循環位數。而分子就是循環節自己。
舉個例子,0.(689)是純循環小數,他的循環部分爲689,總共三位,因此他轉換爲分數的分母就是三個9,分子就是689。轉換成分數就是\(\frac{689}{999}\)。
有了這個定理,前面的題目就能夠求解了:
5.45(689)
= 5 + 0.45 + 0.00(689)
= 5 + \(\frac{45}{100}\) + (0.(689)/100)
= 5 + \(\frac{45}{100}\) + (\(\frac{689}{999}\)/100)
= 5 + \(\frac{45}{100}\) + \(\frac{689}{99900}\)
算到這一步其實就能夠了,咱們已經將它轉化成了分數的加法,只要咱們實現了fc的加法,而後直接調用就好了。因此我這裏代碼的思路是先用正則將循環小數分紅,整數,非循環部分,循環部分,而後用這個計算方法分別轉換成分數,而後加起來就好了。具體的代碼我就不貼了,有興趣的朋友仍是去我GitHub看源碼吧,哈哈。
計算API是最多的一類API,咱們須要支持加,減,乘,除,取餘,次方,開方,絕對值,取反,取倒數,上取整,下取整,四捨五入。同時用戶在計算的時候多是連續計算的,可能加減乘除都有,咱們還須要支持鏈式調用。下面咱們先講講鏈式調用:
鏈式調用在JS的世界裏很常見,好比jQuery,能夠隨意點點點,那這個是怎麼實現的呢?好比以下代碼:
fc(1.5).plus('1/3').times(5).toNumber();
fc(1.5)
返回的是一個fc的實例,爲了可以讓他調到plus
,因此plus
確定得是一個實例方法plus
的返回值還能調到times
方法,那plus
的返回值究竟是什麼呢?答案仍是fc實例,咱們plus
還得返回一個fc實例,times
也是一個實例方法,因此plus
的返回值能訪問。plus
怎麼返回一個fc實例呢?其實很簡單,他本身就是實例方法,是被fc實例調用的,因此這個方法裏面的this就指向了調用者,也就是fc實例。因此要實現鏈式調用,就要在對應的實例方法裏面返回this。若是你對this指向還不是很熟悉,請看這篇文章。下面來看一段鏈式調用的示例代碼:
function fc() {} fc.prototype.func1 = function() { return this;} fc.prototype.func2 = function() { return this;} // 由於實例方法func1和func2都返回了this,因此能夠一直點點點 const instance = new fc(); instance.func1().func2().func2().func1();
上述代碼只是一個鏈式調用演示,並無具體功能,你們能夠根據本身須要添加功能。
咱們的計算API看似有不少,其實核心的就是加法和乘法。由於減法就是加一個符號相反的數,除法就是乘一個倒數。其餘的計算API基本均可以用這兩個核心方法來算。
下面來看看加法,咱們再來回憶下中學數學知識,分數加法的計算:先通分,將分母變成同樣的,而後分子進行相加,而後將最後結果進行約分。看個例子:
\(\frac{1}{2} + \frac{1}{3}\)
= \(\frac{3}{6} + \frac{2}{6}\)
=\(\frac{5}{6}\)
要通分就要計算他們的最小公倍數(lowest common multiple,如下簡稱LCM),要計算最小公倍數其實須要先算最大公約數(greatest common divisor,如下簡稱GCD)。咱們之前算最大公約數,都是將目標數分解成質因數,而後將公共的質因數相乘,就是最大公約數,這個方法比較繁瑣,還要先拆解質因數。咱們這裏不用這個方法,而用歐幾里得算法,上定理:
歐幾里得算法:對於兩個數a, b的最大公約數gcd(a, b)有:
gcd(a, b) = gcd(b, a %b )
仔細看這個公式,你會發現他實際上是能夠迭代的,舉個例子:
gcd(150, 270)
= gcd(270, 150)
= gcd(150, 120)
= gcd(120, 30)
= gcd(30, 0)
迭代到最終的模爲0,其實這時候的"a"就是最終的GCD,咱們這裏就是30,30是150和270的GCD。對於這種能夠迭代的公式,咱們直接一個while循環就搞定了:
function getGCD(a, b) { // get greatest common divisor(GCD) // GCD(a, b) = GCD(b, a % b) a = Math.abs(a); b = Math.abs(b); let mod = a % b; while (mod !== 0) { a = b; b = mod; mod = a % b; } return b; }
拿到了GCD咱們就能夠約分了,也能夠用來算LCM,來看看怎麼算LCM:
對於兩個數a, b, 若是gcd是他們的最大公約數,那麼存在另外兩個互質的數字x, y:
a = x * gcd
b = y * gcd
因此他們的最小公倍數就是 x * y * gcd,也就是
(x * gcd) * (y * gcd) / gcd
= a * b / gcd
有了LCM,咱們的分數加減法就沒有問題了。另外乘法直接分子乘分子,分母乘分母就好了,這裏不展開說了。
還有個須要注意的概念是取餘和取模,也就是咱們計算API裏面的mod
方法。咱們先來看看取餘和取模的區別:
對於兩個正數來講,取餘和取模是沒有區別的,他們的區別在於一個是正數,一個是負數的時候,對於商的取捨上有區別。
取餘: 取餘時,若是除不盡,商往0的方向取整
取模: 取模時,若是除不盡,商往負無窮的方向取整
舉個例子: -7 對 4取餘和取模
- 先算商-1.75
- 取餘,商往0方向取整,也就是-1,而後算 -7 - (-1) * 4 = -3
- 取模,商往負無窮方向取整,也就是-2, 而後算 -7 - (-2) * 4 = 1
JS的%
實際上是取餘計算,因此fc的mod
方法跟他保持了一致,是取餘運算,算法跟前面的例子是同樣的,計算過程當中用到了咱們前面實現的減法和乘法。
其餘幾個計算API都比較簡單,有些仍是基於Math
實現的,好比pow
, ceil
...我這裏就不展開講了,有興趣的朋友仍是去看我GitHub源碼,哈哈~
這幾個比較API都很簡單,直接用本來的數減去目標數就行,減法前面已經實現了。最後將結果跟0比較,能夠輕鬆得出是大於,小於仍是等於。
顯示API有4個,能夠以小數,固定位數小數,循環小數和分數的形式展現。其中toFraction
, toFixed
, toNumber
都比較簡單,toNumber
直接用分子除以分母就行, toFixed
再這個基礎上調一下JS自己的toFixed
就行,toFraction
就是將分子和分母用字符串形式輸出就行,輸出前記得約分。真正有點麻煩的是輸出成循環小數。
將分數轉換成循環小數的方法不止一種,咱們先來講說理論上正確,可是實現起來是坑的方法。
前面循環小數化分數的時候咱們已經講了,對於0.(456)
轉化成分數就是\(\frac{456}{999}\)。那反過來講,只要我將一個分數的分母轉換成n個9的形式,分子不就是循環部分了嗎?那咱們就能夠從一個9開始遍歷,而後到n個9,找到一個能除進的就行,好比:
\(\frac{5}{3}\)
= \(\frac{15}{9}\)
= \(1 + \frac{6}{9}\)
= 1.6666666666...
可是須要注意的是,有些分母的質因子含有2和5,這種一生都轉換不成n個9,對於這種分數,咱們須要對分子乘以10,而後約分,來去掉分母的2和5質因子,若是還去不掉,就再乘10。不要擔憂這裏乘以的10,這裏乘了多少10,最後把小數點往左移動多少位就好了。來個例子:
\(\frac{3}{28}\) // 分母含質因子2,調整分子乘以10
-> \(\frac{30}{28}\)
= \(\frac{15}{14}\) // 分母含質因子2,調整分子乘以10
-> \(\frac{150}{14}\)
= \(\frac{75}{7}\)
= \(10 + \frac{5}{7}\)
= \(10 + \frac{714285}{999999}\)
= 10.714285714285714285714285714285
-> 0.10(714285) // 前面乘了兩個10,小數點左移兩位
上面這個算法理論上來講是正確的,我最開始也是按照這個算法實現的,吭哧吭哧寫了半天代碼,測試的時候遇到了不少詭異的狀況。調試的時候發現,緣由是在計算過程當中,可能須要不少個9的分母,可是JS對於超過20位的數字,直接就四捨五入用科學計數法表示了,後面的計算基於這個確定就不許了:
這條路走不通,只有換條路走,讓咱們從這種「高級」算法中回來,回到咱們質樸的小學數學。咱們學習除法的時候遇到除不盡的時候,都是將餘數乘以10,而後繼續算,那咱們程序也這樣算就行了,那怎麼纔算有循環了呢?有循環的判斷其實就是出現了一樣的餘數。由於出現了一樣的餘數,你後面再用這個數字去乘以10計算,確定跟以前一樣的那個餘數獲得了一樣的結果,這就循環了。想通了這個質樸的道理,咱們只須要將每次計算的餘數存下來,下次計算的時候檢查一下這個餘數是否是存在了,若是已經存在了,那循環節就找到了。這個餘數第一次出現的位置就是循環節開始的位置,第二次出現的前一個位置就是循環節結束的位置。貼個示例代碼吧,爲了加快每次查找的速度,我這裏用的是一個對象來存儲餘數:
function getDecimalsFromFraction(numerator, denominator) { // make sure numerator is less than denominator const modObj = {}; const quotientArray = []; let mod; let index = 0; while (true) { mod = numerator % denominator; if (mod === 0) { return quotientArray.join(''); } let existIndex = modObj[mod]; if (existIndex >= 0) { let quotientLength = quotientArray.length; quotientArray.splice(existIndex, 0, '('); quotientArray.splice(quotientLength + 1, 0, ')'); return quotientArray.join(''); } modObj[mod] = index; index++; numerator = mod * 10; let quotient = parseInt(numerator / denominator); quotientArray.push(quotient); if (index >= 3000) { // Recurring part can be very long, we only handle first 3000 numbers return quotientArray.join(''); } } }
這麼計算的問題是一個分數化循環小數的循環節可能很是長,這個最大長度,理論值是分母-1,由於任何數除以分母,餘數多是1到分母減1之間的任何一個數,運氣很差的時候,可能所有輪一遍。當他很是長的時候,計算很慢,並且沒有必要,因此我這裏只搜索前面3000位小數,若是3000位還沒搜索到,就直接把已有的商返回了。
fc有兩個靜態API,gcd
和lcm
,這其實就是咱們前面計算用到的最大公約數和最小公倍數,既然都寫出來了,爲啥不順便暴露給用戶用呢?
剩下就是clone
了,這其實爲爲了方便用戶想繼續操做,可是又不想修改當前值的時候用。另外還有一個配置,默認輸出分數的時候會約分,加了個開關,能夠輸出不約分的分數。
到這裏,咱們的功能就講完了,下面會說說工程相關的。
單元測試是很重要的,尤爲是對於這種計算庫,我寫完一個功能,須要測試下他功能正常不,就須要單元測試。更重要的是能夠保證重構的正確性,實現過程當中,我屢次踩坑,進行了屢次重構。若是沒有單元測試,重構完我內心是沒譜的,不知道以前的功能有沒有搞壞。有了單元測試,重構完,直接把單元測試拿來跑一遍就行。我這裏單元測試的框架用的Jest,具體使用你們能夠看官方文檔,也能夠看我源碼當例子,我這裏再也不贅述,下面貼一個例子:
describe('FractionCalculator instance', () => { it('can support integer', () => { const instance = fc(4); expect(instance.fraction).toEqual({ numerator: 4, denominator: 1, }); }); });
作了一個工具庫,固然是但願給你們用,造福社會了~打包以前咱們要知道咱們須要一個什麼樣的包,咱們的用戶環境多是什麼樣的,根據具體需求配置打包策略。我這裏的需求是:
- 流行的ES6,node.js要支持
- 瀏覽器要支持
- 老的瀏覽器,好比IE,儘可能支持
根據需求,咱們須要支持import
, require
, script
標籤三種引入方式。好在webpack很強大,咱們只要加一點簡單的配置,就能支持這三種了:
{ ... library: 'fc', // 庫名字,也是script引入時掛載到window的對象名字 libraryTarget: 'umd', // 支持的引入方式,umd是包括ES6, node, 瀏覽器,AMD等 libraryExport: 'default', // 默認導出的路徑,我用export default導出的就寫'default' ... }
另外fc開發的時候用了一些ES6的特性,老瀏覽器是不支持的,因此我還用了babel翻譯下,babel配置也很簡單:
{ ... "useBuiltIns": "usage" // 關鍵就是這個配置,這個只會添加用到了的polyfill ... }
最終我打了三個包出來:
fraction-calculator.js
沒有壓縮,沒有polyfill的版本,供ES6和node使用,package.json裏面的main也指向的這個包,這樣用戶npm安裝以後,import或者require的就是這個文件fraction-calculator.min.js
壓縮版的fraction-calculator.js
,供高級瀏覽器使用,好比火狐,Chrome,高級瀏覽器本身支持ES6,就不用polyfill了,這個文件體積也最小,只有7kbfraction-calculator.polyfill.min.js
加了polyfill的fraction-calculator.min.js
,體積會稍微大一點,供IE之類的使用。這些都弄好後就npm publish
吧,這個命令會將這個庫推送到npm去,而後別人就能夠下載安裝了。
作這個工具起源於偶然間看到的歐幾里得算法,看到這個算法能夠約分,能約分就能計算分數了,那我也寫個分數的加減乘除玩玩。作完這個功能以後,想到還有小數,循環小數呢,因而慢慢加了些功能,就成如今這樣了。最開始的初衷其實不是解決JS浮點數精度問題,作完以後才發現,我靠,這樣一來JS浮點數精度問題不是也解決了嗎,算是意外驚喜了~文中只講了核心方法,其餘方法並無展開講,你們有興趣的能夠看我源碼哦,順便當幫我code review了,哈哈~
文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。本工具剛剛發佈,可能還有一些小bug,若是你在使用中遇到任何問題,能夠直接在GitHub提issue哦。
fc項目GitHub地址: https://github.com/dennis-jiang/fraction-calculator
做者博文GitHub項目地址: https://github.com/dennis-jiang/Front-End-Knowledges