微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

简易的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. 页面结构:

rush:xhtml;">
计算计算

2

2. 结合一点样式:

rush:css;"> 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,.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;
}

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

3. JS逻辑

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

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

rush: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');  

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

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

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

<div class="jb51code">
<pre class="brush:js;">
// 中缀表达式转后缀
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());
  }
},</pre>
rush:js;"> // 后缀表达式计算 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()));
    }
  }
  // 此时结果集中只有<a href="https://www.jb51.cc/tag/yige/" target="_blank" class="keywords">一个</a>值,即为结果
   return this.result[0];
}

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

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

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

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

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

rush:js;">     // 构建中缀表达式 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;
   ...

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

rush:js;"> // 删除操作 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;
  }  

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

rush:js;"> // 添加操作,首先得判断运算符是否重复 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;
  }

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

rush:js;"> // 即时得进行运算 calculate: function(type) { this.infix2Suffix(); var suffixRe = this.calcSuffix();
  if (suffixRe) {
    this.$out.text('=' + suffixRe)
      .attr('title',suffixRe)
      .removeClass('active');

    // 如果是直接<a href="https://www.jb51.cc/tag/xianshi/" target="_blank" class="keywords">显示</a>地进行等于运算
    if (type === 'eq') {
      this.$in.removeClass('active');
      this.$out.addClass('active');
      // 设置<a href="https://www.jb51.cc/tag/biaoji/" target="_blank" class="keywords">标记</a>:当前已经<a href="https://www.jb51.cc/tag/xianshi/" target="_blank" class="keywords">显示</a>地进行计算
      this.calcDone = true;
      this.lastVal = suffixRe;
      // 设置历史记录
      var history = this.infix.join('') + ' = ' + suffixRe;
      this.$history.text(history).attr('title',history);
    }

  }
},</pre>

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

比如点击了数字

rush:js;">       // 数字:0-9 if (!isNaN(parseInt(val,10))) { // 构建中缀表达式并显示 var infixRe = that.buildInfix(parseInt(val,10),'add'); that.$in.text(infixRe.join('')).addClass('active');
        that.cal<a href="https://www.jb51.cc/tag/cula/" target="_blank" class="keywords">cula</a>te();

        return;
      }

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

rush:js;"> // 预运算:百分比、小数点、平方 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.cal<a href="https://www.jb51.cc/tag/cula/" target="_blank" class="keywords">cula</a>te();
      }

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

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

rush:js;"> $(function() {

function Calculator($dom) {
this.$dom = $($dom);
// 历史运算
this.$history = this.$dom.find('.calc-history');
// 输入区
this.$in = this.$dom.find('.calc-in');
// 输出
this.$out = this.$dom.find('.calc-out');
this.$operation = this.$dom.find('.calc-operation');

// 运算符映射
this.op = {
  'plus': '+','minus': '-','mul': '*','div': '/'
};
this.opArr = ['+','-','*','/'];

// 中缀表达式
this.infix = [];
// 后缀表达式
this.suffix = [];
// 后缀表达式运算结果集
this.result = [];
// 存储最近的值
this.lastVal = 0;
// 当前已经计算等于完成
this.calcDone = false;
// 当前<a href="https://www.jb51.cc/tag/zhengzaijinxing/" target="_blank" class="keywords">正在进行</a>小数点点(.)相关值的修正
this.curDot = false;

this.init();

}

Calculator.prototype = {
constructor: Calculator,// 初始化
init: function() {
this.bindEvent();
},// 绑定事件
bindEvent: function() {
var that = this;

  that.$operation.on('click',action;

    if (elem.tagName === 'TD') {
      val = elem.getAttribute('data-val') || elem.getAttribute('data-ac');
      // 数字:0-9
      if (!isNaN(parseInt(val,'add');
        that.$in.text(infixRe.join('')).addClass('active');

        that.cal<a href="https://www.jb51.cc/tag/cula/" target="_blank" class="keywords">cula</a>te();

        return;
      }

      action = val;

      // 操作:清除、<a href="https://www.jb51.cc/tag/shanchu/" target="_blank" class="keywords">删除</a>、计算等于
      if (['cls','del','eq'].indexOf(action) !== -1) {
        if (!that.infix.length) {
          return;
        }

        // 清空数据
        if (action === 'cls' || (action === 'del' && that.calcDone)) {
          that.$in.text('');
          that.$out.text('');

          that.resetData();
        }
        // 清除
        else if (action === 'del') {
          // 重新构建中缀表达式
          var infixRe = that.buildInfix(that.op[action],'del');
          that.$in.text(infixRe.join('')).addClass('active');

          that.cal<a href="https://www.jb51.cc/tag/cula/" target="_blank" class="keywords">cula</a>te();

        }
        // 等于
        else if (action === 'eq') {
          that.cal<a href="https://www.jb51.cc/tag/cula/" target="_blank" class="keywords">cula</a>te('eq');

        }
      }
      // 预运算:百分比、小数点、平方
      else if (['per','change');
        that.$in.text(infixRe.join('')).addClass('active');

        that.cal<a href="https://www.jb51.cc/tag/cula/" target="_blank" class="keywords">cula</a>te();
      }
      // 运算符:+ - * /
      else if (that.isOp(that.op[action])) {
        if (!that.infix.length && (that.op[action] === '*' || that.op[action] === '/')) {
          return;
        }

        var infixRe = that.buildInfix(that.op[action],'add');
        that.$in.text(infixRe.join('')).addClass('active');
      }
    }
  });
},resetData: function() {
  this.infix = [];
  this.suffix = [];
  this.result = [];
  this.lastVal = 0;
  this.curDot = false;
},// 构建中缀表达式
buildInfix: function(val,type) {
  // 直接的点击等于运算之后,
  if (this.calcDone) {
    this.calcDone = false;
    // 再<a href="https://www.jb51.cc/tag/dianjishu/" target="_blank" class="keywords">点击数</a>字,则进行新的运算
    if (!this.isOp(val)) {
      this.resetData();
    }
    // 再点击运算符,则使用当前的结果值继续进行运算
    else {
      var re = this.result[0];
      this.resetData();
      this.infix.push(re);
    }

  }

  var newVal;

  // <a href="https://www.jb51.cc/tag/shanchu/" target="_blank" class="keywords">删除</a>操作
  if (type === 'del') {
    newVal = this.infix.pop();
    // <a href="https://www.jb51.cc/tag/shanchu/" target="_blank" class="keywords">删除</a>末尾一位数
    newVal = Math.floor(newVal / 10);
    if (newVal) {
      this.infix.push(newVal);
    }

    this.lastVal = this.infix[this.infix.length - 1];
    return this.infix;
  }
  // <a href="https://www.jb51.cc/tag/tianjia/" target="_blank" class="keywords">添加</a>操作,首先得判断运算符是否重复
  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;
    }

  // T<a href="https://www.jb51.cc/tag/odo/" target="_blank" class="keywords">odo</a>: 小数点运算
  //   if (this.isOp(val)) {
  //     this.curDot = false;
  //   }

  //   // 小数点
  //   if (this.curDot) {
  //     var dotLen = 0;
  //     newVal = this.infix.pop();
  //     dotLen = newVal.toString().split('.');
  //     dotLen = dotLen[1] ? dotLen[1].length : 0;

  //     newVal += val / Math.pow(10,dotLen + 1);
  //     // 修正小数点运算精确值
  //     newVal = parseFloat(newVal.toFixed(dotLen + 1));

  //     this.infix.push(this.lastVal = newVal);
  //     return this.infix;
  //   }

    this.infix.push(this.lastVal = val);
    return this.infix;
  }

  // 更改操作,比如%的预运算
  else if (type === 'change') {
    this.infix.pop();
    this.infix.push(this.lastVal = val);

    return this.infix;
  }

},// 判断是否为运算符
isOp: function(op) {
  return op && this.opArr.indexOf(op) !== -1;
},// 判断运算符优先级
priorHigher: function(a,b) {
  return (a === '+' || a === '-') && (b === '*' || b === '/');
},// 进行运算符的运算
opCalc: function(b,op,a) {
  return op === '+'
    ? a + b
    : op === '-'
    ? a - b
    : op === '*'
    ? a * b
    : op === '/'
    ? a / b
    : 0;
},// 即时得进行运算
cal<a href="https://www.jb51.cc/tag/cula/" target="_blank" class="keywords">cula</a>te: function(type) {
  this.infix2Suffix();
  var suffixRe = this.calcSuffix();

  if (suffixRe) {
    this.$out.text('=' + suffixRe)
      .attr('title',// 中缀表达式转后缀
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,// 后缀表达式计算
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.result.pop()));
    }
  }
  // 此时结果集中只有<a href="https://www.jb51.cc/tag/yige/" target="_blank" class="keywords">一个</a>值,即为结果
   return this.result[0];
}

};

new Calculator('.calc-wrap');
});

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程之家。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐