简易的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;

}

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

 

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();

}

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

$(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;

// 当前正在进行小数点点(.)相关值的修正

this.curDot = false;

this.init();

}

Calculator.prototype = {

constructor: Calculator,

// 初始化

init: function() {

this.bindEvent();

},

// 绑定事件

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

// 数字: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;

}

action = val;

// 操作:清除、删除、计算等于

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.calculate();

}

// 等于

else if (action === 'eq') {

that.calculate('eq');

}

}

// 预运算:百分比、小数点、平方

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();

}

// 运算符:+ - * /

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;

// 再点击数字,则进行新的运算

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;

}

// TODO: 小数点运算

// 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;

},

// 即时得进行运算

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);

}

}

},

// 中缀表达式转后缀

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];

}

};

new Calculator('.calc-wrap');

});

以上是 简易的JS计算器实现代码 的全部内容, 来源链接: utcz.com/z/325281.html

回到顶部