热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

js实现四则混合运算计算器

最近想用js做一个简单的计算器,不过网上的例子好像大部分都是直接从左到右挨个计算,就好像1+2*5,就会先计算1+2,再计算3*5,并没有实现运算符的优先级,这里找到了一种方法实现,来总结一下。不过这

最近想用js做一个简单的计算器,不过网上的例子好像大部分都是直接从左到右挨个计算,就好像1+2*5,就会先计算1+2,再计算3*5,并没有实现运算符的优先级,这里找到了一种方法实现,来总结一下。不过这里只是最基本的思路,还有许许多多的细节没有完善。

在解决基本的样式布局与交互逻辑之前,我们先来解决四则混合运算的核心模块,也就是如何把我们输入的字符串转换为数字表达式并进行计算:

我所找到的方法叫做逆波兰表达式(也叫做后缀表达式),关于逆波兰表达式的具体定义大家可以上网去搜索一下,概念应该比较简单。

这里我们举一个例子来展示一下逆波兰表达式的作用:

例如:3+4*5 ,这个表达式要如何实现先算乘号再算加号呢?对于计算机来说应该很难实现

但是把它转换成这个式子再看一下:

3,4,5,*,+,那么这样看起来好像就简单多了,只要每遇到一个操作符就将他的前两个操作数进行运算,再将操作结果代替运算表达式直到算出最终结果,这样说有点复杂,我们还是看一下例子

3,4,5,*,+  ->  3,20,+  ->  23

那么该如何把我们熟悉的 3+4*5  转变成这个牛逼的逆波兰表达式呢?

大家也可以上网搜索一下,这里我给出一个我找到的链接:

基于逆波兰表达式的公式解析器

为了方便大家顺畅的浏览文章,这里截取文章的部分核心内容(即转换规则):

一般算法:

       逆波兰表达式的一般解析算法是建立在简单算术表达式上的,它是我们进行公式解析和执行的基础:

       1. 构建两个栈Operand(操作数栈)和Operator(操作符栈)。

       2.扫描给定的字符串,如果得到一个数字,则提取(扫描是一位一位的,一定要提取一个完整的数字)数字(以下用Operand代替),然后把Operand压入Operand栈中。

       3. 如果获得一个运算符(比如+或者*,下面用B代替),则需要和Operator栈栈顶元素(用A替代)比较:

              1) 如果A不存在,则把B压入Operator栈中;

              2)如果B是一个左括号,则忽略A和B的优先级比较,把B压入Operator栈。

              3)如果B是一个右括号,则把Operator栈顺序出栈,然后把弹出的元素顺序压入Operand栈中,直到栈顶弹出的是左括号,括号不入Operand栈中。

              4)如果A是左括号,则把B直接压入Operator栈。

              5)如果B优先级比较A高,则把B直接压入Operator栈。

              6)如果B优先级低于或等于A的优先级,则把A出栈然后压入Operand栈,反复进行此步骤直到栈顶优先级高于B的优先级或者栈顶是一个括号。

       4.扫描完毕后,把Operator栈的元素依次出栈,然后依次压入Operand栈中。

 

虽然不太 明白原理,不过跟着一步步做就可以得到逆波兰表达式了。这里一般会在一开始往operator里面压入一个“#”,并把它的优先级设置为最低,这样就方便其他运算符来进行比较了。

现在我们来理一下思路:为了得到逆波兰表达式我们需要以下几个步骤:

1.将字符串转换为数组,转化过程中要将操作数和操作符分开,直接操作字符串的话,会出现错误,例如:

3+20 会被解析成: 3,+,2,0

 

2.在数组前加一个“#”,方便进行操作符的比较。

3.根据上面给出的规则进行编码得到operant数组

 

首先是字符串的转换:

 

这里是我想出来一种比较笨的方法,就是在操作符两边都加上一个分隔符,在根据这个分隔符来进行分割。应该有更简单的方法,大家可以在评论区讨论下。

   var operand = [], //用于存放操作数的栈
operator = [], //用于存放操作符的栈
textArr = text.split(''),
newTextArr
= [],
calTextArr
= []; //用于存放操作数与操作符分割后的数组。
for(var i = 0; i ){
if(!Number(text[i])){
newTextArr.push(
"|",text[i],"|");
}
else{
newTextArr.push(textArr[i]);
}
}
calTextArr
= newTextArr.join('').split("|");
calTextArr.unshift(
"#")

 

然后就是根据规则一步步来了,但是其中有一个运算符的优先级比较我们还没有解决

 

运算符的优先级比较


这里我们把每一个运算符的优先级都用数字来表示就更加清晰明了。

/*
*比较操作符的优先级
*param string 需要被转换的字符串
*/
function compareOperator(a,b){
var aLevel = getOperatorRand(a),
bLevel
= getOperatorRand(b);

if(aLevel <= bLevel){
return true;
}
else if(aLevel > bLevel){
return false;
}
}

/*
*将操作符的优先级用数字具体化
*/
function getOperatorRand(operator){
switch(operator){
case "#":
return 0;
case "+":
return 1;
break;
case "-":
return 1;
break;
case "*":
return 2;
break;
case "/":
return 2;
break;
}
}

 

 运算符的优先级比较问题也已经解决了,然后我们就可以得到逆波兰表达式了:

得到逆波兰表达式(其中text是待转换的字符串):

function getRPN(text){
var operand = [], //用于存放操作数的栈
operator = [], //用于存放操作符的栈
textArr = text.split(''),
newTextArr
= [];
for(var i = 0; i ){
if(!Number(text[i]) && Number(text[i]) != 0){
newTextArr.push(
"|",text[i],"|");
}
else{
newTextArr.push(textArr[i]);
}
}
var calTextArr = newTextArr.join('').split("|");
calTextArr.unshift(
"#")

for(var i = 0; i ){
//如果是数字则直接入栈
if(Number(calTextArr[i]) || Number(calTextArr[i]) == 0){
operand.push(calTextArr[i]);
}
//如果是操作符则再根据不同的情况进行操作
else {
switch(true){
//如果operator栈顶是“(”或者遍历到的操作符是“(”则直接入栈
case calTextArr[i] == "(" && operator.slice(-1)[0] == "(":
operator.push(calTextArr[i]);
break;

/*如果遍历到的操作符是“)”则把operator中的操作符依次弹出并压入
operand中直至operator栈顶操作符为“(”,然后将“(”也弹出,但不压入
operand栈中
*/
case calTextArr[i] == ")":
do{
operator.push(operator.pop());
}
while(operator.slice(-1)[0] != "(");
operator.pop();
break;

//如果是其他的操作符,则比较优先级后再进行操作
default:
var compare = compareOperator(calTextArr[i],operator.slice(-1)[0]);
var a = calTextArr[i];
var b = operator.slice(-1)[0]
if(operator.length == 0){
operator.push(calTextArr[i]);
}
else if(compareOperator(calTextArr[i],operator.slice(-1)[0])){
do{
operand.push(operator.pop());
var compareResult = compareOperator(calTextArr[i],operator.slice(-1)[0]);
}
while(compareResult);
operator.push(calTextArr[i]);
}
else {
operator.push(calTextArr[i]);
}
break;
}
}
}
//遍历结束后,将operator中的元素全部压入operand中
operator.forEach(function(){
operand.push(operator.pop());
});
//把用于比较的“#”字符去掉
operator.pop();
return operand;
}

 

ok,逆波兰表达式我们已经搞定了,接下来就可以计算了,计算的思路一开始也讲过了,就是每遇到一个操作符就将他的前两个操作数进行运算,再将操作结果代替运算表达式,这样循环下去直到算出最终结果,不过这里我的代码显得有点多而且麻烦,本人水平有限,有更好的方法请在评论区指出。

/*
*计算并返回结果
*/
function getResult(RPNarr){
var result;
while(RPNarr.length > 1)
RPNarr
= singleResult(RPNarr);
console.log(RPNarr)
result
= RPNarr[0];
return result;
}


/*
*每遇到一个操作符就进行一次运算然后更新数组,直到算出最终结果。
*/
function singleResult(RPNarr){
for(var i = 0,max = RPNarr.length; i ){
console.log(!Number(RPNarr))
if(!Number(RPNarr[i])){
switch(RPNarr[i]){
case "+":
var addResult = Number(RPNarr[i-2]) + Number(RPNarr[i-1]);
RPNarr.splice(i
-2,3,addResult);
return RPNarr;
break;
case "-":
var addResult = Number(RPNarr[i-2]) - Number(RPNarr[i-1]);
RPNarr.splice(i
-2,3,addResult);
return RPNarr;
break;
case "*":
var addResult = Number(RPNarr[i-2]) * Number(RPNarr[i-1]);
RPNarr.splice(i
-2,3,addResult)
return RPNarr;
break;
case "/":
var addResult = Number(RPNarr[i-2]) / Number(RPNarr[i-1]);
RPNarr.splice(i
-2,3,addResult)
return RPNarr;
break;
}
}
}
}

至此,我们的运算过程就全部结束了。

 

然后就是页面的布局,布局和样式大家可以随意实现这里就简单贴下代码:

 

基本布局与样式:

html代码:

DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
head>
<body>
<div id="calculator-container">
<h1>Bett's Calculatorh1>
<div class="calculator-display">
<input id="calculator-display" type="text">
div>
<ul class="btn-list">
<li class="btn-item">1li>
<li class="btn-item">2li>
<li class="btn-item">3li>
<li class="btn-item">+li>
<li class="btn-item">4li>
<li class="btn-item">5li>
<li class="btn-item">6li>
<li class="btn-item">-li>
<li class="btn-item">7li>
<li class="btn-item">8li>
<li class="btn-item">9li>
<li class="btn-item">*li>
<li class="btn-item">.li>
<li class="btn-item">0li>
<li class="btn-item" id="result">=li>
<li class="btn-item">/li>
ul>
div>
<script src="calculator.js">script>
body>
html>

css代码:

html,body,button,h1,h2,div,p,input,ul,li {
margin
: 0;
padding
: 0;
}
ul,li
{
list-style
: none;
}

h1
{
text-align
: center;
color
: #fff;
margin-bottom
: 20px;
font-weight
: normal;
}
#calculator-container
{
width
: 300px;
height
: auto;
margin
: 100px auto 0;
padding
: 20px 20px;
background-color
: #354B69;
overflow
: hidden;
}
.calculator-display
{
width
: 100%;
margin-bottom
: 10px;
}
#calculator-display
{
display
: block;
width
: 100%;
padding
: 5px 10px 5px 0;
text-align
: right;
font-size
: 18px;
line-height
: 30px;
border
: none;
box-sizing
: border-box;
}
.btn-item
{
float
: left;
width
: 24%;
padding
: 6% 0;
margin
: 0.5%;
text-align
: center;
font-size
: 18px;
font-weight
: bold;
color
: #354B69;
background-color
: #fff;
}

最后的效果图是长这样:

有许多功能还没有实现,比如清空,退格,括号的运算等,都还没加进去,有一些小bug可能自己也没发现,这里主要讲解一下思路,大家后面可以自己拓展,我做出完整功能后也会来更新。

 

基本的交互逻辑的思路:

/**

1、首先我需要将我点击的任意一个按钮(除了等号按钮外)的文本显示在input框中
2、在按下等号时将input框中的文本拿出来
3、将得到的字符串转换成数学表达式并进行计算
4、将计算结果反应在input框中
5、计算完成后若再次点击的是数字则清空显示框再进行下一次运算,如果点击的是操作符则继续进行运算
*/

那么这里首先就有一个问题,就是关于计算状态的判断,根据上面的思路我们可以有这样一个思路(input框的value值用val表示):

a. 如果我点击的是普通按钮,那么显示框中就用val+=不断追加并更新内容,我们把这个状态称为“continue”(计算中)

b. 如果我点击的是等号按钮,那么就算出结果result,然后清空搜索框再更新结果,val = result  我们把这个状态称为“end”(计算结束)

但是因为有了第5步,我们的状态判断变得更为复杂了一些:

在按下等号之后:又会出现两种状态:
a. 如果我点击的是操作符,那么就继续进行运算,状态更新为“continue”
b. 如果我点击的是数字,那么就重新开始运算,这里我们给一个新状态“start”(“重新开始运算”)

那我们怎么去判断等号后的下一个按钮是什么呢?感觉很难判断,那我们干脆就在点击普通按钮的时候都来进行一个状态的判断,根据得到的状态来决定如何在显示框中进行显示。这里给出代码:

设置状态

首先我们设置一个全局变量“state”,默认状态为“start”

var calState = "start";

 

状态判断(其中参数text是点击的按钮的文本内容)

/*
*点击普通按钮时进行状态判断
*如果是“continue”状态则继续运算
*如果是“end”状态,则再根据操作的不同进行判断
*/
function setCalState(text){
if(calState == 'end' && Number(text) ||
(calState
== 'end' && Number(text) == 0)){
calState
= 'start';
}
else if(calState == 'end' && !Number(text)){
calState
= 'continue';
}
else {
calState
= 'continue';
}
}

根据状态的不同显示框中的显示也有不同的方式:

function setInputValue(text){
var calInput = document.getElementById('calculator-display');
if(calState == "end" || calState == "start"){
calInput.value
= text
}
else{
calInput.value
+= text;
}
}

利用事件冒泡原理给每一个li 绑定一个点击事件

/*
*利用事件冒泡给每一个按钮添加点击事件
*/
function btnHandleClick(callback){
var btnList = document.getElementsByClassName('btn-list')[0];
btnList.onclick
= function(e){
var btnEl = e.target || window.e.target;
if(btnEl.id == 'result'){
calState
= 'end';
var resultText = getInputValue();
var RPNarr = getRPN(resultText);
var totalResult = getResult(RPNarr);
callback(
'');
callback(totalResult);
}
else{
var btnText = btnEl.innerText;
setCalState(btnText);
callback(btnText);
}
}
}

btnHandleClick(setInputValue);

 至此我们就用js完成了一个简单的四则混合运算的计算器,不过还有一些缺陷,比如说js在进行加减乘除时会有一些精度的问题,比如0.1+0.2 != 0.3,而是等于

0.30000000000000004,类似这样的精度问题,其实可以把getResult中每个操作符的运算抽离出来,例如加法的运算可以单独拿出来做一些处理
写成function add(){} ,然后再进行一些精度的处理。

一种更加简单但不推荐的方法
其实还有一种简单的方法,就是eval(text) ,输入字符串后字符串中的语句就会被自动执行,一行代码就搞定,相当方便,可是高程上并不推荐这种做法
说是不太安全,万一人家输入什么乱七八糟的字符串也会被执行,另外一种 new Function(str)的方法相对安全点,但也是类似的思路
而且最重要的是如果运用了这种方法的话,我们就无法自己对运算过程进行操作了,比如说上面的加法运算的精度问题,还有其他的一些问题,所以我们还
是采用自己实现混合运算的方法。

非常重要的一点:上面的代码只是一个思路,有许多细节部分都没有处理,比如除数不能为0等地方的错误处理也没有。希望大家看的时候能注意下。
本人是一只小白,上述部分如有错漏之处,或者说有更好的思路、方法请在评论区指出

推荐阅读
  • 本文探讨了如何在Classic ASP中实现与PHP的hash_hmac('SHA256', $message, pack('H*', $secret))函数等效的哈希生成方法。通过分析不同实现方式及其产生的差异,提供了一种使用Microsoft .NET Framework的解决方案。 ... [详细]
  • JavaScript中的数组是数据集合的核心结构之一,内置了多种实用的方法。掌握这些方法不仅能提高开发效率,还能显著提升代码的质量和可读性。本文将详细介绍数组的创建方式及常见操作方法。 ... [详细]
  • 本文将继续探讨前端开发中常见的算法问题,重点介绍如何将多维数组转换为一维数组以及验证字符串中的括号是否成对出现。通过多种实现方法的解析,帮助开发者更好地理解和掌握这些技巧。 ... [详细]
  • 本文介绍如何使用 Angular 6 的 HttpClient 模块来获取 HTTP 响应头,包括代码示例和常见问题的解决方案。 ... [详细]
  • Redux入门指南
    本文介绍Redux的基本概念和工作原理,帮助初学者理解如何使用Redux管理应用程序的状态。Redux是一个用于JavaScript应用的状态管理库,特别适用于React项目。 ... [详细]
  • Java 实现二维极点算法
    本文介绍了一种使用 Java 编程语言实现的二维极点算法。该算法用于从一组二维坐标中筛选出极点,适用于需要处理几何图形和空间数据的应用场景。文章不仅详细解释了算法的工作原理,还提供了完整的代码示例。 ... [详细]
  • 本文介绍如何利用栈数据结构在C++中判断字符串中的括号是否匹配。通过顺序栈和链栈两种方式实现,并详细解释了算法的核心思想和具体实现步骤。 ... [详细]
  • 云函数与数据库API实现增删查改的对比
    本文将深入探讨使用云函数和数据库API实现数据操作(增删查改)的不同方法,通过详细的代码示例帮助读者更好地理解和掌握这些技术。文章不仅提供代码实现,还解释了每种方法的特点和适用场景。 ... [详细]
  • 黑马头条项目:Vue 文章详情模块与交互功能实现
    本文详细介绍了如何在黑马头条项目中配置文章详情模块的路由、获取和展示文章详情数据,以及实现关注、点赞、不喜欢和评论功能。通过这些步骤,您可以全面了解如何开发一个完整的前端文章详情页面。 ... [详细]
  • 本文介绍了如何在 C# 和 XNA 框架中实现一个自定义的 3x3 矩阵类(MMatrix33),旨在深入理解矩阵运算及其应用场景。该类参考了 AS3 Starling 和其他相关资源,以确保算法的准确性和高效性。 ... [详细]
  • This post discusses an issue encountered while using the @name annotation in documentation generation, specifically regarding nested class processing and unexpected output. ... [详细]
  • 由二叉树到贪心算法
    二叉树很重要树是数据结构中的重中之重,尤其以各类二叉树为学习的难点。单就面试而言,在 ... [详细]
  • 本文将详细探讨 Java 中提供的不可变集合(如 `Collections.unmodifiableXXX`)和同步集合(如 `Collections.synchronizedXXX`)的实现原理及使用方法,帮助开发者更好地理解和应用这些工具。 ... [详细]
  • 本文档汇总了Python编程的基础与高级面试题目,涵盖语言特性、数据结构、算法以及Web开发等多个方面,旨在帮助开发者全面掌握Python核心知识。 ... [详细]
  • Vue 3.0 翻牌数字组件使用指南
    本文详细介绍了如何在 Vue 3.0 中使用翻牌数字组件,包括其基本设置和高级配置,旨在帮助开发者快速掌握并应用这一动态视觉效果。 ... [详细]
author-avatar
渣渣
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有