首頁 > 軟體

JavaScript實現計算器的四則運算功能

2022-02-08 19:00:44

一、需求 + 最終實現

注:只是前端實現

1. 需求

需求來源是因為有一個做嵌入式 C/C++的基友做了一個遠端計算器。 需求是要求支援輸入一個四則混合運算公式的字串,返回計算後的結果。

想看看用 C/C++封裝過的 JavaScript 如果要實現這樣一個功能最終效果(文章後我會討論這兩種實現思路,還望各位看官可以提出一些優化方案以及建議之類的~)。

2. 說明:利用了字串(split、replace)和陣列(splice)的方法。

主要是用到字串切分為陣列的方法 split、以及陣列中插入刪除的方法 splice。 字串正則 replace 方法是考慮到使用者的輸入習慣可能有所不同,例如 1+2*3/4 與 3 * 7 + 229。

支援:

  • 基礎四則運算 3+6*5/6-3;
  • 小數四則運算 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);
  • 其他數學運算

3. 程式碼實現

/**
 * js四則混合運算計算器  功能實現(約20行+ 麵條程式碼)
 * @param {string} str 輸入的四則運算字串
 * @return {number} 輸出 結果
 */
const calculator = (str) => {
  // 定義新增字元函數
  const add = (arr, symbol) => {
    let length = arr.length;
    while (length > 1) {
      arr.splice(length - 1, 0, symbol); // 在每一項後面新增對應的運運算元
      length--;
    }
    return arr; // 目的是得到一個改變長度的陣列
  }
  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])));
    }
  })
  // 再執行加減法,即從左至右的計算
  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 語法還算熟悉的話,應該可以輕鬆閱讀程式碼的。 想必你也注意到了,這也是其中令我比較糾結的:在日常開發中,是否該經常寫一些麵條程式碼呢?

二、實現步驟

(輕鬆理解的大佬可以直接跳到:步驟3)

1.實現最基礎的加減乘除

2.支援高位數的運算

3.支援多次的運算

4.支援...

如果是初學者,建議跟著敲一下過程(或者 f12 驗證 + 偵錯),程式設計能力某種角度下一定是建立在程式碼量之下的。

1. 版本一:實現基礎加減乘除

// 版本一
const calculator = ((str) => {
  // 定義最基礎的加減乘除
  const add = (a, b) => a + b;
  const sub = (a, b) => a - b;
  const mul = (a, b) => a * b;
  const div = (a, b) => a / b;
  // 將輸入的字串處理為 陣列
  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

這樣就實現了一個四則混合運算的計算器!

但是這個計算器很雞肋,只是一個最基礎的功能上的實現。即:只可以執行一位數數位的加減乘除混合運算。

其實第一步的想法是,利用陣列的性質,通過運算元組來操作單次的四則運算。其中陣列的遍歷,我優先 *, / 法,緊接著是 +,- 法。 這其實是有問題的,乘除法在實際運算中的優先順序並不明顯,可以說是不怎麼影響運算的結果(在文章最後一個版本實現涉及到效能上的討論時會詳談),但是加減法就會有影響了:必須是從左至右的實現,否則影響運算的結果(這裡不多贅述)。

【處理基本四則運算】

首先處理字串為陣列 const array = str.split('');這一步程式碼舉例說明:

(圖一)

  1. 在處理字串的時候,可以看到 '3+6*5/6-3' 處理成了 ['3', '+', '6', '*', '5', '/', '6', '-', '3']。
  2. 然後在版本一程式碼中,可以看到我處理運算的執行順序是 ['*', '/', '+', '-'],所以版本一隻支援加減乘除一次運算;
  3. const index = array.findIndex(o => o === it); 這一步找到步驟 2 中的符號所在陣列中的位置(說明一下,只用字串的方法也可以實現,即找到字串的位置,然後操作也可,只是陣列更常用,也更容易理解)
  4. 觀察處理後的陣列,符號總是隔一位出現的,即便是優先順序較高的 *、/ 法,也是符號所在的位置的前一項與後一項的運算結果。 array[index + 1] = mul(array[index - 1], array[index + 1]); 將符號所在的下一項的值為呼叫對應的操作函數的運算結果;
  5. 刪除符號位與第一項:array.splice(index - 1, 2)
  6. 這時候可以看到最初定義的 array 陣列一直在改變,以 node 環境下的列印結果為例(注意觀察運算陣列):

(圖二)

可以看到每次列印都會列印初始陣列以及通過 splice 方法處理之後的結果。

弊端:此版本不支援多次運算,即四則混合運算只能執行一次。同時,也不能夠支援高位運算。

2. 版本二:實現高位數的運算

在圖一中

如果是涉及高位(個位以上)的數值運算字串的話,單純的使用 split('') 方法會把兩位數數值,處理成陣列的兩項,即影響運算結果。

所以我需要一個方法,在接收一個字串以後,得到我想要的字串:

(圖三)

如圖三所述, ary 即所需。

所以,圖三中由 str 到 ary 的過程就是本次版本所需要實現的:

/**
 * 實現字串的陣列化分割
 * @param {string} strs 輸入的字串 : '12*33/3+9+10'
 * @returns 陣列 ['12', '*', '33', '/',  '3', '+', '9',  '+', '10']
 */
const split = () => {
  const result = str.split('+')  // 遇到 + 處理為陣列
    .map(it => {
      return it.split('-') // 遇到 - 處理為陣列
        .map(it => {
          return it.split('*') // 遇到 * 處理為陣列
            .map(it => {
              return it.split('/') // 遇到 / 處理為陣列
            })
        })
    })
  return result.flat(3);
}

我在設計這個演演算法的時候,一時間也沒有太好的思路和想法,該函數處理字串為一個多維陣列,然後再將陣列扁平化處理。如圖四所示:

(圖四)

圖四中,執行該函數,得到一個多維陣列(其實最高也只有三維陣列),返回值 result 列印出來的結果可以看到,基本滿足所需要的陣列:['31', '+', '62', '*', '5', '/', '6', '-', '3'] 。

接下來,為其帶上運運算元:

/**
 * 定義新增字元函數
 * @param {string[]} result 傳入的陣列 ['31', '62*5/6-3']
 * @param {string} symbol 傳入的運運算元
 * @returns 陣列 ['31', '+', '62*5/6-3']
 */
  const add = (result, symbol) => {
    let length = result.length;
    while (length !== 1) {
      result.splice(length - 1, 0, symbol); // 在每一項後面新增對應的運運算元
      length--;
    }
    return result; // 目的是得到一個改變長度的陣列
  }

比如傳入 ['31', '62*5/6-3'] ,只需要在第一項之後補 '+' 即可。

實現的目的是考慮到多次運算的時候,為每一個因為 '+' 分割的陣列中的項新增運運算元,所以這裡用到了 while 迴圈語句,並且由一個變數 length 控制(也可以遍歷陣列或者 for 迴圈陣列實現這一步操作);

檢驗結果,如圖五所示:

(圖五)

這樣就實現了這個任意長度數值陣列輸入時,返回帶符號的陣列。

【回顧一下】:

上面兩個函數的整體實現就是,實現了根據符號分割陣列,根據傳入的陣列與符號新增符號:

結合兩個函數,並且簡化一下程式碼(其實我個人還是喜歡寫麵條程式碼的,只是可能不利於閱讀,但是看起來舒服一些~):

  // 定義新增字元函數
  const add = (result, symbol) => {
    let length = result.length;
    while (length !== 1) {
      result.splice(length - 1, 0, symbol); // 在每一項後面新增對應的運運算元
      length--;
    }
    return result; // 目的是得到一個改變長度的陣列
  }
  const array = (strs = str) =>
    add(strs.split('+'), '+').map(it =>
      add(it.split('-'), '-').map(it =>
        add(it.split('*'), '*').map(it =>
          add(it.split('/'), '/')
        )
      )
    ).flat(3);

即,任意運算字串的傳入都可以處理為所需陣列如圖六所示:

(圖六)

array 函數在後面直接把內部處理常式的返回值繫結了。

對於上述演演算法的設計如果有更好的實現還希望有朋友可以指出,大家互相之間可以學習一下。

3. 支援多次的運算

回到版本一,目前的實現只支援一次的四則混合運算,更合理的實現應該是先運算乘除法,再運算加減法,而且先出現的先執行。

完整運算程式碼:

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])));
  }
  // 再執行加減法,即從左至右的計算
  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, "") (去除空格)。
等等,我剛剛想到了什麼?

如果大家都在輸入的時候,自覺加一個空格隔開運運算元與數值的話~

是不是我之前版本二中的字串處理就可以省一下啦!!

所以作為開發者,一定要 注意規範,注意規範,注意規範!

上面完整程式碼中,

  1. 簡化了呼叫加減乘除函數,改而用 array.splice(index - 1, 3, 運算) 運算直接可以操作兩引數。
  2. 得到了可運算元組 array 後,先執行乘除法,再執行加減法。
  3. 乘除法裡先判斷 是否存在 * 或 / 兩個符號,如果存在,則找到符號的位置,運算每一個乘除法,按數學的思維,誰在前先運算誰(但我依然規定了先運算所有的 *,再運算所有的 / 這種方式作為最終實現並放到了文章最開始。因為真正在運算的時候,乘除法的先後執行順序得到的結果似乎並沒有什麼關係,而與我而言,我感覺在這套實現中,includes 與 find 的多次執行可能對效能上的損耗更大一些)
  4. 當所有的乘除法執行完畢後,就只剩下加減法了,這時候按順序執行加減法即可。
  5. 最後保留兩位小數。

其實這段程式碼更符合數學思維,先運算乘除法(誰在前先運算誰),再運算加減法。

如果大家有一些其他的想法,可以一起討論一下~

三、思考

後端思維

1. 實現逆波蘭表示式

1+2*3 這是一箇中綴表示式,人腦很容易計算,結果為7。當然計算機也很容易處理這個表示式。

當我們輸入1.2+(-1+3*1)*2,人腦需要思考一下,但計算機還是可以通過固定程式碼快速計算出結果。

但是,當我們隨機輸入中綴表示式 XXX 時,人腦可以手動計算出結果,計算機不可能一個表示式一個程式碼塊,那麼計算機怎麼實現通用且快速的計算呢?答案就是字尾表示式。

中綴和字尾表示式在資料結構裡有涉及到,我就不講概念了,下面手動模擬一下計算機計算字串表示式的過程。

2. 中綴表示式 => 字尾表示式

計算機易於計算的其實是字尾表示式,整個過程就是將已知的中綴表示式轉換為字尾表示式。

2.1 定義【運算元棧】和【運運算元棧】:

2.2 運運算元棧出棧,運算元棧入棧,上式即可成為: 123*+ 這就是一個簡單的字尾表示式

2.3 計算機在運算字尾表示式時:運運算元棧讀取 *,運算元棧讀取 2,3 得到結果 6,;然後運算 1 + 6 = 7。

3. 較複雜的表示式計算

入棧:

( 後的- 作為負數進入運算元棧(如果作為符號位,後面計算會成1.1 - 30);

與上文一樣,只是不同之處在於運運算元棧,遇到 ( 以後先進入運運算元棧;

直到遇到 )

3.1 使用 # 符號區分 負數、高位數、以及符號位

3.2 所以得到的字尾表示式為: #-1.1#3#10#*#+#2#/#

出棧過程:

  • 計算沒有遇到符號位一 # 為準,依次取出
  • 直到遇到符號位之前,取出了三個數 -1.1 3 10
  • 遇到符號位以後,在結果棧中出棧,與符號計算結果 => 結果棧變為 -1.1 30
  • 依此計算:

  • 出棧完成後,就實現了對逆波蘭表示式的求值運算。

前端思維

我拿到【實現一個支援四則混合運算的計算器】需求以後,首先想到的是字串轉陣列,然後去運算元組,然後由於高階語言的特性,很多方法已經封裝完成,所以實現起來相對容易一些。

當然,也可以採用前端的程式碼,用著後端的思維去實現也是一個選擇。

結束

其實這個計算器與電腦中的常規計算器並無區別,後期可以考慮的升級方式

  • 實現 () 的優先順序功能;
  • 其他數學計算等等...

總結一下就是,後端的實現在效能上無與倫比,尤其是程式碼的執行速度上,我這裡沒有測試資料,但是如果你有刷力扣的話,你可以看看同樣的演演算法,JS 的空間複雜度【記憶體消耗】,是 C/C++ 等更底層的語言消耗數倍。

同樣的,如果用 C/C++ 底層語言 + 後端思維 去實現【開闢記憶體】、將中綴表示式轉換為字尾表示式所用定義的【運算元棧】、【運運算元棧】;以及各種棧頂棧底的【指標操作】;外加如果交由使用者使用涉及到的設定【代理】,網路協定封裝等等... (最終總程式碼量數百行)

我將之稱為業務複雜度(hhh)對比前端 20行+ 的程式碼實現~

更底層語言需要考慮的東西比較多,所以實現起來花費的人力相對更多,同樣的收穫的對電腦效能消耗價效比也是前端 JS 不可比擬的

不當之處還望各位指正~

以上就是JavaScript實現計算器的四則運算功能的詳細內容,更多關於JavaScript計算器的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com