你的位置:首页 > Java教程

[Java教程]JS实现简易的计算器


JS可以做的事多了,那就用来实现一个计算器吧

 

看看手机中的计算器,分为普通计算器和科学计算器

   

 

自认脑袋不够大,就实现一个普通版本的吧(支持正负数加减乘除等基本连续的运算,未提供括号功能)

看看图示效果, 或 在线演示

 

一、知识准备

1+1 = ?

正常来说,我们看到这个表达式都知道怎么运算,知道运算结果

但计算机不一样,计算机无法识别出这串表达式,它只能识别特定的规则:前缀表达式+ 1 1 或后缀表达式1 1 +

举个栗子

(3 + 4) × 5 - 6 就是中缀表达式
- × + 3 4 5 6 前缀表达式
3 4 + 5 × 6 - 后缀表达式

 

所以为了实现程序的自动运算,我们需要将输入的数据转化为前缀或后缀表达式

前缀、中缀、后缀表达式的概念以及相互转换方法在这里就不多说了,这篇博文 说得比较清楚了 

所以,在这个计算器的实现中,采用了后缀表达式的实现方式,参考以上文章,重点关注这两个算法:

与转换为前缀表达式相似,遵循以下步骤:(1) 初始化两个栈:运算符栈S1和储存中间结果的栈S2;(2) 从左至右扫描中缀表达式;(3) 遇到操作数时,将其压入S2;(4) 遇到运算符时,比较其与S1栈顶运算符的优先级:(4-1) 如果S1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;(4-2) 否则,若优先级比栈顶运算符的高,也将运算符压入S1(注意转换为前缀表达式时是优先级较高或相同,而这里则不包括相同的情况);(4-3) 否则,将S1栈顶的运算符弹出并压入到S2中,再次转到(4-1)与S1中新的栈顶运算符相比较;(5) 遇到括号时:(5-1) 如果是左括号“(”,则直接压入S1;(5-2) 如果是右括号“)”,则依次弹出S1栈顶的运算符,并压入S2,直到遇到左括号为止,此时将这一对括号丢弃;(6) 重复步骤(2)至(5),直到表达式的最右边;(7) 将S1中剩余的运算符依次弹出并压入S2;(8) 依次弹出S2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式(转换为前缀表达式时不用逆序)。

将中缀表达式转换为后缀表达式:
与前缀表达式类似,只是顺序是从左至右:从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 op 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。例如后缀表达式“3 4 + 5 × 6 -”:(1) 从左至右扫描,将3和4压入堆栈;(2) 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素,注意与前缀表达式做比较),计算出3+4的值,得7,再将7入栈;(3) 将5入栈;(4) 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;(5) 将6入栈;(6) 最后是-运算符,计算出35-6的值,即29,由此得出最终结果。

后缀表达式的计算机求值:

 

二、实现过程

第一步当然是搭建计算器的页面结构,不是科学计算器,只提供了基本的运算功能,但也能即时地进行运算,显示出完整的中缀表达式,运算后保存上一条运算记录。

要先说一下:本来想实现小数点功能的,但小数点的存在让数据存储与数据显示的实现有了压力,实现过程实在脑大,索性先取消这个功能。

 

1. 页面结构:

  <h5>计算计算</h5>  <!-- 计算器 -->  <div class="calc-wrap">    <div class="calc-in-out">      <!-- 上一条运算记录 -->      <p class="calc-history" title=""></p>      <!-- 输入的数据 -->      <p class="calc-in"></p>      <!-- 输出的运算结果 -->      <p class="calc-out active"></p>    </div>    <table class="calc-operation">      <thead></thead>      <tbody>        <tr>          <td data-ac="cls" class="cls">C</td>          <td data-ac="del">&larr;</td>          <td data-ac="sq">x<sup>2</sup></td>          <td data-ac="mul">&times;</td>        </tr>        <tr>          <td data-val="7">7</td>          <td data-val="8">8</td>          <td data-val="9">9</td>          <td data-ac="div">&divide;</td>        </tr>        <tr>          <td data-val="4">4</td>          <td data-val="5">5</td>          <td data-val="6">6</td>          <td data-ac="plus">+</td>        </tr>        <tr>          <td data-val="1">1</td>          <td data-val="2">2</td>          <td data-val="3">3</td>          <td data-ac="minus">-</td>        </tr>          <td data-ac="per">%</td>          <td data-val="0">0</td>          <td data-ac="dot">.</td>          <td data-ac="eq" class="eq">=</td>      </tbody>    </table>  </div>

2. 结合一点样式:

body {  padding: 20px;  font-family: Arial;}.calc-wrap {  width: 300px;  border: 1px solid #ddd;  border-radius: 3px;}.calc-operation {  width: 100%;  border-collapse: collapse;}.calc-in-out {  width: 100%;  padding: 10px 20px;  text-align: right;  box-sizing: border-box;  background-color: rgba(250, 250, 250, .9);}.calc-in-out p {  overflow: hidden;  margin: 5px;  width: 100%;}.calc-history {  margin-left: -20px;  font-size: 18px;  color: #bbb;  border-bottom: 1px dotted #ddf;  min-height: 23px;}.calc-in,.calc-out {  font-size: 20px;  color: #888;  line-height: 39px;  min-height: 39px;}.calc-in {  color: #888;}.calc-out {  color: #ccc;}.calc-in.active,.calc-out.active {  font-size: 34px;  color: #666;}.calc-operation td {  padding: 10px;  width: 25%;  text-align: center;  border: 1px solid #ddd;  font-size: 26px;  color: #888;  cursor: pointer;}.calc-operation td:active {  background-color: #ddd;}.calc-operation .cls {  color: #ee8956;}

CSS样式

这样静态的计算器就粗来了~~

 

3. JS逻辑

这部分就是重点了,一步步来说

首先是对计算器的监听吧,也就是这个表格,可以使用事件委托的方式,在父级节点上监听处理

    // 绑定事件    bindEvent: function() {      var that = this;      that.$operation.on('click', function(e) {        e = e || window.event;        var elem = e.target || e.srcElement,          val,          action;        if (elem.tagName === 'TD') {          val = elem.getAttribute('data-val') || elem.getAttribute('data-ac');              ...

监听数据,获取到的只是页面上的某个值/操作符,所以需要将数据存储起来形成中缀,再由中缀转换成后缀,最后通过后缀进行计算

    // 中缀表达式    this.infix = [];    // 后缀表达式    this.suffix = [];    // 后缀表达式运算结果集    this.result = [];

按照算法步骤,实现出来,这里没有使用到括号,如果实际需要,可在相应位置修改判断条件即可~

    // 中缀表达式转后缀    infix2Suffix: function() {      var temp = [];      this.suffix = [];      for (var i = 0; i < this.infix.length; i++) {        // 数值,直接压入        if (!this.isOp(this.infix[i])) {          this.suffix.push(this.infix[i]);        }        else {          if (!temp.length) {            temp.push(this.infix[i]);          }          else {            var opTop = temp[temp.length - 1];            // 循环判断运算符优先级,将运算符较高的压入后缀表达式            if (!this.priorHigher(opTop, this.infix[i])) {              while (temp.length && !this.priorHigher(opTop, this.infix[i])) {                this.suffix.push(temp.pop());                opTop = temp[temp.length - 1];              }            }              // 将当前运算符也压入后缀表达式            temp.push(this.infix[i]);          }        }      }      // 将剩余运算符号压入      while (temp.length) {        this.suffix.push(temp.pop());      }    },

    // 后缀表达式计算    calcSuffix: function() {      this.result = [];      for (var i = 0; i < this.suffix.length; i++) {        // 数值,直接压入结果集        if (!this.isOp(this.suffix[i])) {          this.result.push(this.suffix[i]);        }        // 运算符,从结果集中取出两项进行运算,并将运算结果置入结果集合        else {          this.result.push(this.opCalc(this.result.pop(), this.suffix[i], this.result.pop()));        }      }      // 此时结果集中只有一个值,即为结果       return this.result[0];    }

其实,在实现的时候会发现,中缀、后缀只是一个难点,更复杂的地方是整个计算器的状态变化(或者说是数据变化)

在这个简单的计算器中,就有数字(0-9)、运算符(+ - * /)、操作(清除 删除)、预运算(百分号 平方)、小数点、即时运算等数据及操作

如果是科学计算器那就更复杂了,所以理清如何控制这些东西很关键,而其中最重要的就是中缀表达式的构建与存储

 

当连续点击+号时,是不符合实际操作的,所以需要一个变量 lastVal 来记录上一个值,随着操作而更新,再通过判断,防止程序出错

在点击=号之后,我们可以继续使用这个结果进行运算,或者重新开始运算

    // 构建中缀表达式    buildInfix: function(val, type) {      // 直接的点击等于运算之后,      if (this.calcDone) {        this.calcDone = false;        // 再点击数字,则进行新的运算        if (!this.isOp(val)) {          this.resetData();        }        // 再点击运算符,则使用当前的结果值继续进行运算        else {          var re = this.result[0];          this.resetData();          this.infix.push(re);        }      }      var newVal;       ...

点击删除,是删除一位数,不是直接地删除一个数,然后更新中缀表达式的值

      // 删除操作      if (type === 'del') {        newVal = this.infix.pop();        // 删除末尾一位数        newVal = Math.floor(newVal / 10);        if (newVal) {          this.infix.push(newVal);        }        this.lastVal = this.infix[this.infix.length - 1];        return this.infix;      }  

而添加操作,要考虑的就更多了,比如连续的连续运算符、连续的数字、运算符+ - 接上数字表示正负数,小数点的连接存取等

      // 添加操作,首先得判断运算符是否重复      else if (type === 'add') {        // 两个连续的运算符        if (this.isOp(val) && this.isOp(this.lastVal)) {          return this.infix;        }        // 两个连续的数字        else if (!this.isOp(val) && !this.isOp(this.lastVal)) {          newVal = this.lastVal * 10 + val;          this.infix.pop();          this.infix.push(this.lastVal = newVal);          return this.infix;        }        // 首个数字正负数        if (!this.isOp(val) && this.infix.length === 1 && (this.lastVal === '+' || this.lastVal === '-')) {          newVal = this.lastVal === '+' ? val : 0 - val;          this.infix.pop();          this.infix.push(this.lastVal = newVal);          return this.infix;        }        this.infix.push(this.lastVal = val);        return this.infix;      }

在很多次操作的时候,计算器都需要即时地进行运算,为简化代码,可以封装成一个方法,在相应的位置调用即可

    // 即时得进行运算    calculate: function(type) {      this.infix2Suffix();      var suffixRe = this.calcSuffix();      if (suffixRe) {        this.$out.text('=' + suffixRe)          .attr('title', suffixRe)          .removeClass('active');        // 如果是直接显示地进行等于运算        if (type === 'eq') {          this.$in.removeClass('active');          this.$out.addClass('active');          // 设置标记:当前已经显示地进行计算          this.calcDone = true;          this.lastVal = suffixRe;          // 设置历史记录          var history = this.infix.join('') + ' = ' + suffixRe;          this.$history.text(history).attr('title', history);        }      }    },

剩下的就是点击之后的处理过程了,也就是各种调用处理 传递数据->构建中缀处理数据->中缀转后缀->后缀运算显示

比如点击了数字

          // 数字:0-9          if (!isNaN(parseInt(val, 10))) {            // 构建中缀表达式并显示            var infixRe = that.buildInfix(parseInt(val, 10), 'add');            that.$in.text(infixRe.join('')).addClass('active');            that.calculate();            return;          }

又比如几个预运算,其实长得也差不多

        // 预运算:百分比、小数点、平方          else if (['per', 'dot', 'sq'].indexOf(action) !== -1) {            if (!that.infix.length || that.isOp(that.lastVal)) {              return;            }            if (action === 'per') {              that.lastVal /= 100;            } else if (action === 'sq') {              that.lastVal *= that.lastVal;            } else if (action === 'dot') {              // that.curDot = true;            }            // 重新构建中缀表达式            var infixRe = that.buildInfix(that.lastVal, 'change');            that.$in.text(infixRe.join('')).addClass('active');            that.calculate();          }

 

以上就是这个简单计算器的实现步骤了,变化太多还不敢保证不会出错

基本逻辑如此,如果要加上小数点运算、括号运算、正余弦等科学计算器的功能,还是自己去实现吧。。脑大啊。。

 

 1 $(function() { 2  3   function Calculator($dom) { 4     this.$dom = $($dom); 5     // 历史运算 6     this.$history = this.$dom.find('.calc-history'); 7     // 输入区 8     this.$in = this.$dom.find('.calc-in'); 9     // 输出区 10     this.$out = this.$dom.find('.calc-out'); 11     this.$operation = this.$dom.find('.calc-operation'); 12  13     // 运算符映射 14     this.op = { 15       'plus': '+', 16       'minus': '-', 17       'mul': '*', 18       'div': '/' 19     }; 20     this.opArr = ['+', '-', '*', '/']; 21  22     // 中缀表达式 23     this.infix = []; 24     // 后缀表达式 25     this.suffix = []; 26     // 后缀表达式运算结果集 27     this.result = []; 28     // 存储最近的值 29     this.lastVal = 0; 30     // 当前已经计算等于完成 31     this.calcDone = false; 32     // 当前正在进行小数点点(.)相关值的修正 33     this.curDot = false; 34  35     this.init(); 36   } 37  38   Calculator.prototype = { 39     constructor: Calculator, 40     // 初始化 41     init: function() { 42       this.bindEvent(); 43     }, 44     // 绑定事件 45     bindEvent: function() { 46       var that = this; 47  48       that.$operation.on('click', function(e) { 49         e = e || window.event; 50         var elem = e.target || e.srcElement, 51           val, 52           action; 53  54         if (elem.tagName === 'TD') { 55           val = elem.getAttribute('data-val') || elem.getAttribute('data-ac'); 56           // 数字:0-9 57           if (!isNaN(parseInt(val, 10))) { 58             // 构建中缀表达式并显示 59             var infixRe = that.buildInfix(parseInt(val, 10), 'add'); 60             that.$in.text(infixRe.join('')).addClass('active'); 61  62             that.calculate(); 63  64             return; 65           } 66  67           action = val; 68  69           // 操作:清除、删除、计算等于 70           if (['cls', 'del', 'eq'].indexOf(action) !== -1) { 71             if (!that.infix.length) { 72               return; 73             } 74  75             // 清空数据 76             if (action === 'cls' || (action === 'del' && that.calcDone)) { 77               that.$in.text(''); 78               that.$out.text(''); 79  80               that.resetData(); 81             } 82             // 清除 83             else if (action === 'del') { 84               // 重新构建中缀表达式 85               var infixRe = that.buildInfix(that.op[action], 'del'); 86               that.$in.text(infixRe.join('')).addClass('active'); 87  88               that.calculate(); 89  90             } 91             // 等于 92             else if (action === 'eq') { 93               that.calculate('eq'); 94  95             } 96           } 97           // 预运算:百分比、小数点、平方 98           else if (['per', 'dot', 'sq'].indexOf(action) !== -1) { 99             if (!that.infix.length || that.isOp(that.lastVal)) {100               return;101             }102 103             if (action === 'per') {104               that.lastVal /= 100;105             } else if (action === 'sq') {106               that.lastVal *= that.lastVal;107             } else if (action === 'dot') {108               // that.curDot = true;109             }110 111             // 重新构建中缀表达式112             var infixRe = that.buildInfix(that.lastVal, 'change');113             that.$in.text(infixRe.join('')).addClass('active');114 115             that.calculate();116           }117           // 运算符:+ - * /118           else if (that.isOp(that.op[action])) {119             if (!that.infix.length && (that.op[action] === '*' || that.op[action] === '/')) {120               return;121             }122 123             var infixRe = that.buildInfix(that.op[action], 'add');124             that.$in.text(infixRe.join('')).addClass('active');125           }126         }127       });128     },129 130     resetData: function() {131       this.infix = [];132       this.suffix = [];133       this.result = [];134       this.lastVal = 0;135       this.curDot = false;136     },137 138     // 构建中缀表达式139     buildInfix: function(val, type) {140       // 直接的点击等于运算之后,141       if (this.calcDone) {142         this.calcDone = false;143         // 再点击数字,则进行新的运算144         if (!this.isOp(val)) {145           this.resetData();146         }147         // 再点击运算符,则使用当前的结果值继续进行运算148         else {149           var re = this.result[0];150           this.resetData();151           this.infix.push(re);152         }153 154       }155 156       var newVal;157 158       // 删除操作159       if (type === 'del') {160         newVal = this.infix.pop();161         // 删除末尾一位数162         newVal = Math.floor(newVal / 10);163         if (newVal) {164           this.infix.push(newVal);165         }166 167         this.lastVal = this.infix[this.infix.length - 1];168         return this.infix;169       }170       // 添加操作,首先得判断运算符是否重复171       else if (type === 'add') {172         // 两个连续的运算符173         if (this.isOp(val) && this.isOp(this.lastVal)) {174           return this.infix;175         }176         // 两个连续的数字177         else if (!this.isOp(val) && !this.isOp(this.lastVal)) {178           newVal = this.lastVal * 10 + val;179           this.infix.pop();180           this.infix.push(this.lastVal = newVal);181 182           return this.infix;183         }184         // 首个数字正负数185         if (!this.isOp(val) && this.infix.length === 1 && (this.lastVal === '+' || this.lastVal === '-')) {186           newVal = this.lastVal === '+' ? val : 0 - val;187           this.infix.pop();188           this.infix.push(this.lastVal = newVal);189 190           return this.infix;191         }192 193       // TODO: 小数点运算194       //   if (this.isOp(val)) {195       //     this.curDot = false;196       //   }197 198       //   // 小数点199       //   if (this.curDot) {200       //     var dotLen = 0;201       //     newVal = this.infix.pop();202       //     dotLen = newVal.toString().split('.');203       //     dotLen = dotLen[1] ? dotLen[1].length : 0;204 205       //     newVal += val / Math.pow(10, dotLen + 1);206       //     // 修正小数点运算精确值207       //     newVal = parseFloat(newVal.toFixed(dotLen + 1));208 209       //     this.infix.push(this.lastVal = newVal);210       //     return this.infix;211       //   }212 213         this.infix.push(this.lastVal = val);214         return this.infix;215       }216 217       // 更改操作,比如%的预运算218       else if (type === 'change') {219         this.infix.pop();220         this.infix.push(this.lastVal = val);221 222         return this.infix;223       }224 225     },226     // 判断是否为运算符227     isOp: function(op) {228       return op && this.opArr.indexOf(op) !== -1;229     },230     // 判断运算符优先级231     priorHigher: function(a, b) {232       return (a === '+' || a === '-') && (b === '*' || b === '/');233     },234     // 进行运算符的运算235     opCalc: function(b, op, a) {236       return op === '+'237         ? a + b238         : op === '-'239         ? a - b240         : op === '*'241         ? a * b242         : op === '/'243         ? a / b244         : 0;245     },246     // 即时得进行运算247     calculate: function(type) {248       this.infix2Suffix();249       var suffixRe = this.calcSuffix();250 251       if (suffixRe) {252         this.$out.text('=' + suffixRe)253           .attr('title', suffixRe)254           .removeClass('active');255 256         // 如果是直接显示地进行等于运算257         if (type === 'eq') {258           this.$in.removeClass('active');259           this.$out.addClass('active');260           // 设置标记:当前已经显示地进行计算261           this.calcDone = true;262           this.lastVal = suffixRe;263           // 设置历史记录264           var history = this.infix.join('') + ' = ' + suffixRe;265           this.$history.text(history).attr('title', history);266         }267 268       }269     },270 271     // 中缀表达式转后缀272     infix2Suffix: function() {273       var temp = [];274       this.suffix = [];275 276       for (var i = 0; i < this.infix.length; i++) {277         // 数值,直接压入278         if (!this.isOp(this.infix[i])) {279           this.suffix.push(this.infix[i]);280         }281         else {282           if (!temp.length) {283             temp.push(this.infix[i]);284           }285           else {286             var opTop = temp[temp.length - 1];287             // 循环判断运算符优先级,将运算符较高的压入后缀表达式288             if (!this.priorHigher(opTop, this.infix[i])) {289               while (temp.length && !this.priorHigher(opTop, this.infix[i])) {290                 this.suffix.push(temp.pop());291                 opTop = temp[temp.length - 1];292               }293             }294              // 将当前运算符也压入后缀表达式295             temp.push(this.infix[i]);296           }297         }298       }299       // 将剩余运算符号压入300       while (temp.length) {301         this.suffix.push(temp.pop());302       }303     },304 305     // 后缀表达式计算306     calcSuffix: function() {307       this.result = [];308 309       for (var i = 0; i < this.suffix.length; i++) {310         // 数值,直接压入结果集311         if (!this.isOp(this.suffix[i])) {312           this.result.push(this.suffix[i]);313         }314         // 运算符,从结果集中取出两项进行运算,并将运算结果置入结果集合315         else {316           this.result.push(this.opCalc(this.result.pop(), this.suffix[i], this.result.pop()));317         }318       }319       // 此时结果集中只有一个值,即为结果320        return this.result[0];321     }322   };323 324   new Calculator('.calc-wrap');325 });

完整JS