JavaScript實現(xiàn)計算器的四則運算功能
一、需求 + 最終實現(xiàn)
注:只是前端實現(xiàn)
1. 需求
需求來源是因為有一個做嵌入式 C/C++的基友做了一個遠程計算器。 需求是要求支持輸入一個四則混合運算公式的字符串,返回計算后的結(jié)果。
想看看用 C/C++封裝過的 JavaScript 如果要實現(xiàn)這樣一個功能最終效果(文章后我會討論這兩種實現(xiàn)思路,還望各位看官可以提出一些優(yōu)化方案以及建議之類的~)。
2. 說明:利用了字符串(split、replace)和數(shù)組(splice)的方法。
主要是用到字符串切分為數(shù)組的方法 split、以及數(shù)組中插入刪除的方法 splice。 字符串正則 replace 方法是考慮到用戶的輸入習慣可能有所不同,例如 1+2*3/4 與 3 * 7 + 229。
支持:
- 基礎(chǔ)四則運算 3+6*5/6-3;
- 小數(shù)四則運算 3.14 + 6 * 5 / 6 - 3.5;
- 高位四則運算 99 * 94 - 6.35 + 100 / 1024;
- 多次四則運算 3 * 3 + 3 * 16 - 7 - 5 + 4 / 2 + 22;
- 以上綜合
不支持:
- 帶括號的運算 1 * (2 - 3);
- 其他數(shù)學運算
3. 代碼實現(xiàn)
/**
* js四則混合運算計算器 功能實現(xiàn)(約20行+ 面條代碼)
* @param {string} str 輸入的四則運算字符串
* @return {number} 輸出 結(jié)果
*/
const calculator = (str) => {
// 定義添加字符函數(shù)
const add = (arr, symbol) => {
let length = arr.length;
while (length > 1) {
arr.splice(length - 1, 0, symbol); // 在每一項后面添加對應(yīng)的運算符
length--;
}
return arr; // 目的是得到一個改變長度的數(shù)組
}
const array = add(str.replace(/\s*/g,"").split('+'), '+').map(it => add(it.split('-'), '-').map(it => add(it.split('*'), '*').map(it => add(it.split('/'), '/')))).flat(3);;
// 先運算乘除法
['*', '/'].map(it => {
while (array.includes(it)) {
const index = array.findIndex(o => o === it);
index > 0 && it === '*' ? array.splice(index - 1, 3, (Number(array[index - 1]) * Number(array[index + 1]))) : array.splice(index - 1, 3, (Number(array[index - 1]) / Number(array[index + 1])));
}
})
// 再執(zhí)行加減法,即從左至右的計算
while (array.length > 1) {
array[1] === '+' ? array.splice(0, 3, (Number(array[0]) + Number(array[2]))) : array.splice(0, 3, (Number(array[0]) - Number(array[2])));
}
return Number(array[0]).toFixed(2);
}如果對 ES6 語法還算熟悉的話,應(yīng)該可以輕松閱讀代碼的。 想必你也注意到了,這也是其中令我比較糾結(jié)的:在日常開發(fā)中,是否該經(jīng)常寫一些面條代碼呢?
二、實現(xiàn)步驟
(輕松理解的大佬可以直接跳到:步驟3)
1.實現(xiàn)最基礎(chǔ)的加減乘除
2.支持高位數(shù)的運算
3.支持多次的運算
4.支持...
如果是初學者,建議跟著敲一下過程(或者 f12 驗證 + 調(diào)試),編程能力某種角度下一定是建立在代碼量之下的。
1. 版本一:實現(xiàn)基礎(chǔ)加減乘除
// 版本一
const calculator = ((str) => {
// 定義最基礎(chǔ)的加減乘除
const add = (a, b) => a + b;
const sub = (a, b) => a - b;
const mul = (a, b) => a * b;
const div = (a, b) => a / b;
// 將輸入的字符串處理為 數(shù)組
const array = str.split('');
// **【處理基本四則運算】
['*', '/', '+', '-'].map(it => {
const index = array.findIndex(o => o === it);
if (index > 0) {
switch (it) {
case '*':
array[index + 1] = mul(array[index - 1], array[index + 1]);
break;
case '/':
array[index + 1] = div(array[index - 1], array[index + 1]);
break;
case '+':
array[index + 1] = add(Number(array[index - 1]), Number(array[index + 1]));
break;
case '-':
array[index + 1] = sub(Number(array[index - 1]), Number(array[index + 1]));
break;
}
array.splice(index - 1, 2)
}
})
// return array[0];
console.log('返回值:', array[0]);
})('3+6*5/6-3')
// 返回值: 5這樣就實現(xiàn)了一個四則混合運算的計算器!
但是這個計算器很雞肋,只是一個最基礎(chǔ)的功能上的實現(xiàn)。即:只可以運行一位數(shù)數(shù)字的加減乘除混合運算。
其實第一步的想法是,利用數(shù)組的性質(zhì),通過操作數(shù)組來操作單次的四則運算。其中數(shù)組的遍歷,我優(yōu)先 *, / 法,緊接著是 +,- 法。 這其實是有問題的,乘除法在實際運算中的優(yōu)先級并不明顯,可以說是不怎么影響運算的結(jié)果(在文章最后一個版本實現(xiàn)涉及到性能上的討論時會詳談),但是加減法就會有影響了:必須是從左至右的實現(xiàn),否則影響運算的結(jié)果(這里不多贅述)。
【處理基本四則運算】
首先處理字符串為數(shù)組 const array = str.split('');這一步代碼舉例說明:

(圖一)
- 在處理字符串的時候,可以看到 '3+6*5/6-3' 處理成了 ['3', '+', '6', '*', '5', '/', '6', '-', '3']。
- 然后在版本一代碼中,可以看到我處理運算的執(zhí)行順序是 ['*', '/', '+', '-'],所以版本一只支持加減乘除一次運算;
- const index = array.findIndex(o => o === it); 這一步找到步驟 2 中的符號所在數(shù)組中的位置(說明一下,只用字符串的方法也可以實現(xiàn),即找到字符串的位置,然后操作也可,只是數(shù)組更常用,也更容易理解)
- 觀察處理后的數(shù)組,符號總是隔一位出現(xiàn)的,即便是優(yōu)先級較高的 *、/ 法,也是符號所在的位置的前一項與后一項的運算結(jié)果。 array[index + 1] = mul(array[index - 1], array[index + 1]); 將符號所在的下一項的值為調(diào)用對應(yīng)的操作函數(shù)的運算結(jié)果;
- 刪除符號位與第一項:array.splice(index - 1, 2)
- 這時候可以看到最初定義的 array 數(shù)組一直在改變,以 node 環(huán)境下的打印結(jié)果為例(注意觀察運算數(shù)組):

(圖二)
可以看到每次打印都會打印初始數(shù)組以及通過 splice 方法處理之后的結(jié)果。
弊端:此版本不支持多次運算,即四則混合運算只能執(zhí)行一次。同時,也不能夠支持高位運算。
2. 版本二:實現(xiàn)高位數(shù)的運算
在圖一中

如果是涉及高位(個位以上)的數(shù)值運算字符串的話,單純的使用 split('') 方法會把兩位數(shù)數(shù)值,處理成數(shù)組的兩項,即影響運算結(jié)果。
所以我需要一個方法,在接收一個字符串以后,得到我想要的字符串:

(圖三)
如圖三所述, ary 即所需。
所以,圖三中由 str 到 ary 的過程就是本次版本所需要實現(xiàn)的:
/**
* 實現(xiàn)字符串的數(shù)組化分割
* @param {string} strs 輸入的字符串 : '12*33/3+9+10'
* @returns 數(shù)組 ['12', '*', '33', '/', '3', '+', '9', '+', '10']
*/
const split = () => {
const result = str.split('+') // 遇到 + 處理為數(shù)組
.map(it => {
return it.split('-') // 遇到 - 處理為數(shù)組
.map(it => {
return it.split('*') // 遇到 * 處理為數(shù)組
.map(it => {
return it.split('/') // 遇到 / 處理為數(shù)組
})
})
})
return result.flat(3);
}我在設(shè)計這個算法的時候,一時間也沒有太好的思路和想法,該函數(shù)處理字符串為一個多維數(shù)組,然后再將數(shù)組扁平化處理。如圖四所示:

(圖四)
圖四中,執(zhí)行該函數(shù),得到一個多維數(shù)組(其實最高也只有三維數(shù)組),返回值 result 打印出來的結(jié)果可以看到,基本滿足所需要的數(shù)組:['31', '+', '62', '*', '5', '/', '6', '-', '3'] 。
接下來,為其帶上運算符:
/**
* 定義添加字符函數(shù)
* @param {string[]} result 傳入的數(shù)組 ['31', '62*5/6-3']
* @param {string} symbol 傳入的運算符
* @returns 數(shù)組 ['31', '+', '62*5/6-3']
*/
const add = (result, symbol) => {
let length = result.length;
while (length !== 1) {
result.splice(length - 1, 0, symbol); // 在每一項后面添加對應(yīng)的運算符
length--;
}
return result; // 目的是得到一個改變長度的數(shù)組
}比如傳入 ['31', '62*5/6-3'] ,只需要在第一項之后補 '+' 即可。
實現(xiàn)的目的是考慮到多次運算的時候,為每一個因為 '+' 分割的數(shù)組中的項添加運算符,所以這里用到了 while 循環(huán)語句,并且由一個變量 length 控制(也可以遍歷數(shù)組或者 for 循環(huán)數(shù)組實現(xiàn)這一步操作);
檢驗結(jié)果,如圖五所示:

(圖五)
這樣就實現(xiàn)了這個任意長度數(shù)值數(shù)組輸入時,返回帶符號的數(shù)組。
【回顧一下】:
上面兩個函數(shù)的整體實現(xiàn)就是,實現(xiàn)了根據(jù)符號分割數(shù)組,根據(jù)傳入的數(shù)組與符號添加符號:
結(jié)合兩個函數(shù),并且簡化一下代碼(其實我個人還是喜歡寫面條代碼的,只是可能不利于閱讀,但是看起來舒服一些~):
// 定義添加字符函數(shù)
const add = (result, symbol) => {
let length = result.length;
while (length !== 1) {
result.splice(length - 1, 0, symbol); // 在每一項后面添加對應(yīng)的運算符
length--;
}
return result; // 目的是得到一個改變長度的數(shù)組
}
const array = (strs = str) =>
add(strs.split('+'), '+').map(it =>
add(it.split('-'), '-').map(it =>
add(it.split('*'), '*').map(it =>
add(it.split('/'), '/')
)
)
).flat(3);即,任意運算字符串的傳入都可以處理為所需數(shù)組如圖六所示:

(圖六)
array 函數(shù)在后面直接把內(nèi)部處理函數(shù)的返回值綁定了。
對于上述算法的設(shè)計如果有更好的實現(xiàn)還希望有朋友可以指出,大家互相之間可以學習一下。
3. 支持多次的運算
回到版本一,目前的實現(xiàn)只支持一次的四則混合運算,更合理的實現(xiàn)應(yīng)該是先運算乘除法,再運算加減法,而且先出現(xiàn)的先執(zhí)行。
完整運算代碼:
const calculator = (str) => {
const add = (result, symbol) => {
let length = result.length;
while (length > 1) {
result.splice(length - 1, 0, symbol);
length--;
}
return result;
}
const array = add(str.replace(/\s*/g, "").split('+'), '+').map(it => add(it.split('-'), '-').map(it => add(it.split('*'), '*').map(it => add(it.split('/'), '/')))).flat(3);;
// 先運算乘除法
while (array.includes('*') || array.includes('/')) {
const itSymbol = array.find(o => o === '*' || o === '/');
const index = array.findIndex(o => o === '*' || o === '/');
index > 0 && itSymbol === '*' ? array.splice(index - 1, 3, (Number(array[index - 1]) * Number(array[index + 1]))) : array.splice(index - 1, 3, (Number(array[index - 1]) / Number(array[index + 1])));
}
// 再執(zhí)行加減法,即從左至右的計算
while (array.length > 1) {
array[1] === '+' ? array.splice(0, 3, (Number(array[0]) + Number(array[2]))) : array.splice(0, 3, (Number(array[0]) - Number(array[2])));
}
return Number(array[0]).toFixed(2);
}注:有必要說明一下,因為個人習慣不同,所以輸入帶有空格情況,所以這里在處理字符串之前首先用到了一個正則表達式 str.replace(/\s*/g, "") (去除空格)。
等等,我剛剛想到了什么?

如果大家都在輸入的時候,自覺加一個空格隔開運算符與數(shù)值的話~
是不是我之前版本二中的字符串處理就可以省一下啦?。?/p>
所以作為開發(fā)者,一定要 注意規(guī)范,注意規(guī)范,注意規(guī)范!
上面完整代碼中,
- 簡化了調(diào)用加減乘除函數(shù),改而用 array.splice(index - 1, 3, 運算) 運算直接可以操作兩參數(shù)。
- 得到了可操作數(shù)組 array 后,先執(zhí)行乘除法,再執(zhí)行加減法。
- 乘除法里先判斷 是否存在 * 或 / 兩個符號,如果存在,則找到符號的位置,運算每一個乘除法,按數(shù)學的思維,誰在前先運算誰(但我依然規(guī)定了先運算所有的 *,再運算所有的 / 這種方式作為最終實現(xiàn)并放到了文章最開始。因為真正在運算的時候,乘除法的先后執(zhí)行順序得到的結(jié)果似乎并沒有什么關(guān)系,而與我而言,我感覺在這套實現(xiàn)中,includes 與 find 的多次執(zhí)行可能對性能上的損耗更大一些)
- 當所有的乘除法執(zhí)行完畢后,就只剩下加減法了,這時候按順序執(zhí)行加減法即可。
- 最后保留兩位小數(shù)。
其實這段代碼更符合數(shù)學思維,先運算乘除法(誰在前先運算誰),再運算加減法。
如果大家有一些其他的想法,可以一起討論一下~
三、思考
后端思維
1. 實現(xiàn)逆波蘭表達式
1+2*3 這是一個中綴表達式,人腦很容易計算,結(jié)果為7。當然計算機也很容易處理這個表達式。
當我們輸入1.2+(-1+3*1)*2,人腦需要思考一下,但計算機還是可以通過固定代碼快速計算出結(jié)果。
但是,當我們隨機輸入中綴表達式 XXX 時,人腦可以手動計算出結(jié)果,計算機不可能一個表達式一個代碼塊,那么計算機怎么實現(xiàn)通用且快速的計算呢?答案就是后綴表達式。
中綴和后綴表達式在數(shù)據(jù)結(jié)構(gòu)里有涉及到,我就不講概念了,下面手動模擬一下計算機計算字符串表達式的過程。
2. 中綴表達式 => 后綴表達式
計算機易于計算的其實是后綴表達式,整個過程就是將已知的中綴表達式轉(zhuǎn)換為后綴表達式。
2.1 定義【操作數(shù)?!亢汀具\算符?!浚?/p>

2.2 運算符棧出棧,操作數(shù)棧入棧,上式即可成為: 123*+ 這就是一個簡單的后綴表達式
2.3 計算機在運算后綴表達式時:運算符棧讀取 *,操作數(shù)棧讀取 2,3 得到結(jié)果 6,;然后運算 1 + 6 = 7。
3. 較復雜的表達式計算
入棧:
( 后的- 作為負數(shù)進入操作數(shù)棧(如果作為符號位,后面計算會成1.1 - 30);
與上文一樣,只是不同之處在于運算符棧,遇到 ( 以后先進入運算符棧;

直到遇到 )

3.1 使用 # 符號區(qū)分 負數(shù)、高位數(shù)、以及符號位

3.2 所以得到的后綴表達式為: #-1.1#3#10#*#+#2#/#;

出棧過程:
- 計算沒有遇到符號位一
#為準,依次取出 - 直到遇到符號位之前,取出了三個數(shù)
-1.1 3 10 - 遇到符號位以后,在結(jié)果棧中出棧,與符號計算結(jié)果 => 結(jié)果棧變?yōu)?
-1.1 30 - 依此計算:

- 出棧完成后,就實現(xiàn)了對逆波蘭表達式的求值運算。
前端思維
我拿到【實現(xiàn)一個支持四則混合運算的計算器】需求以后,首先想到的是字符串轉(zhuǎn)數(shù)組,然后去操作數(shù)組,然后由于高級語言的特性,很多方法已經(jīng)封裝完成,所以實現(xiàn)起來相對容易一些。
當然,也可以采用前端的代碼,用著后端的思維去實現(xiàn)也是一個選擇。
結(jié)束
其實這個計算器與電腦中的常規(guī)計算器并無區(qū)別,后期可以考慮的升級方式
- 實現(xiàn)
(與)的優(yōu)先級功能; - 其他數(shù)學計算等等...
總結(jié)一下就是,后端的實現(xiàn)在性能上無與倫比,尤其是代碼的執(zhí)行速度上,我這里沒有測試數(shù)據(jù),但是如果你有刷力扣的話,你可以看看同樣的算法,JS 的空間復雜度【內(nèi)存消耗】,是 C/C++ 等更底層的語言消耗數(shù)倍。
同樣的,如果用 C/C++ 底層語言 + 后端思維 去實現(xiàn)【開辟內(nèi)存】、將中綴表達式轉(zhuǎn)換為后綴表達式所用定義的【操作數(shù)?!?、【運算符?!?;以及各種棧頂棧底的【指針操作】;外加如果交由用戶使用涉及到的設(shè)置【代理】,網(wǎng)絡(luò)協(xié)議封裝等等... (最終總代碼量數(shù)百行)
我將之稱為業(yè)務(wù)復雜度(hhh)對比前端 20行+ 的代碼實現(xiàn)~
更底層語言需要考慮的東西比較多,所以實現(xiàn)起來花費的人力相對更多,同樣的收獲的對電腦性能消耗性價比也是前端 JS 不可比擬的
不當之處還望各位指正~
以上就是JavaScript實現(xiàn)計算器的四則運算功能的詳細內(nèi)容,更多關(guān)于JavaScript計算器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于js中window.location.href,location.href,parent.location.href
關(guān)于js中window.location.href,location.href,parent.location.href,top.location.href的用法2010-10-10
JS實現(xiàn)的字符串數(shù)組去重功能小結(jié)
這篇文章主要介紹了JS實現(xiàn)的字符串數(shù)組去重功能,結(jié)合實例形式分析了javascript基于ES6、ES5、ES3及正則實現(xiàn)數(shù)組去重的相關(guān)操作技巧,需要的朋友可以參考下2019-06-06
JavaScript的設(shè)計模式經(jīng)典之代理模式
代理模式的定義是把對一個對象的訪問, 交給另一個代理對象來操作。接下來通過本文給大家介紹JavaScript的設(shè)計模式之代理模式,感興趣的朋友一起學習吧2016-02-02
頁面縮放兼容性處理方法(zoom,Firefox火狐瀏覽器)
下面小編就為大家?guī)硪黄撁婵s放兼容性處理方法(zoom,Firefox火狐瀏覽器)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
微信小程序?qū)崿F(xiàn)列表頁的點贊和取消點贊功能
這篇文章主要為大家詳細介紹了微信小程序?qū)崿F(xiàn)列表頁的點贊和取消點贊功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-11-11

