JavaScript实现计算器四则运算:从基础算法到逆波兰表达式解析

作者:青柠

时间:2026-10-25

浏览:273

本文详细解析了使用JavaScript实现四则混合运算计算器的两种思路。首先通过字符串分割与数组操作实现基础版计算器,支持多位数与混合运算;随后深入探讨后端思维下的逆波兰表达式算法,讲解中缀表达式转后缀表达式的原理及栈的应用,对比前后端实现的性能与复杂度差异。

文章目录

一、需求与最终实现方案

注:本文仅探讨前端实现部分。

1. 需求分析

该需求的灵感来源于一位从事嵌入式C/C++开发的同事所实现的远程计算器。核心需求是:接收一个包含四则混合运算公式的字符串,并返回计算后的结果值。

作者希望探索在使用C/C++封装的JavaScript环境下,实现此类功能的最终效果(文章末尾将讨论这两种实现思路,欢迎读者提出优化方案与建议)。

2. 技术说明:基于字符串与数组操作

主要利用字符串的split、replace方法以及数组的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计算器的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章