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">←</td>
<td data-ac="sq">x<sup>2</sup></td>
<td data-ac="mul">×</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">÷</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();
}
以上就是这个简单计算器的实现步骤了,变化太多还不敢保证不会出错
基本逻辑如此,如果要加上小数点运算、括号运算、正余弦等科学计算器的功能,还是自己去实现吧。。脑大啊。。
$(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
JS实现简易的计算器的更多相关文章
- HTML、CSS与JS实现简易iPhone计算器
效果如图 源码,通俗易懂 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> ...
- html、css、js实现简易计算器
学习HTML,CSS,JS一个月后,想着能自己是否能写出一个简单的东西,故编写了简易的计算器,之前也写过一个坦克大战,坦克大战的有些基本功能没有实现, 故也没有记录下来,想来,对这行初来咋到的,还是需 ...
- 使用html+css+js实现简易计算器
使用html+css+js实现简易计算器, 效果图如下: html代码如下: <!DOCTYPE html> <html lang="en"> <he ...
- 项目:JS实现简易计算器案例
组件化网页开发下的: 步骤一:让页面动起来的JavaScript深入讲解 的 项目:JS实现简易计算器案例
- 03-c#入门(简易存款利息计算器v1.0)
本想把练习题做了的结果放上来,不过发现附录是有答案的,就算了吧,自己做了没问题就行了哈.之前提到过,要是有朋友有想法,需要做小工具我可以帮忙实现,不过貌似大家都很忙.SO,自己学完第4章后,决定做一个 ...
- 使用Node.js实现简易MVC框架
在使用Node.js搭建静态资源服务器一文中我们完成了服务器对静态资源请求的处理,但并未涉及动态请求,目前还无法根据客户端发出的不同请求而返回个性化的内容.单靠静态资源岂能撑得起这些复杂的网站应用,本 ...
- [转] 使用Node.js实现简易MVC框架
在使用Node.js搭建静态资源服务器一文中我们完成了服务器对静态资源请求的处理,但并未涉及动态请求,目前还无法根据客户端发出的不同请求而返回个性化的内容.单靠静态资源岂能撑得起这些复杂的网站应用,本 ...
- 原生JS实现简易轮播图
原生JS实现简易轮播图(渐变?) 最近做网页总是会用到轮播图,我就把之前写的轮播图单独拿出来吧,如果有...如果真的有人也需要也可以复制去用用啊..哈~.. window.onload = funct ...
- node.js 开发简易的小爬虫
node.js 开发简易的小爬虫 最近公司开发一款医药类的软件,所以需要一些药品的基础数据,所以本人就用node.js写一个简易的小爬虫,并写记录这个Demo以供大家参考. 一.开发前的准备: 1, ...
随机推荐
- MSDN Kinect for Windows SDK中文版论坛开放了
昨天MSDN上K4W开发相关的中文版论坛正式开放了,地址 http://social.msdn.microsoft.com/Forums/zh-CN/home?forum=k4wdevelopment ...
- java线程与并发(二)
一般而言,线程通常有以下的这么几个状态: 创建状态:准备好了一个多线程操作对象 就绪状态:调用了start()方法,等待CPU调度 运行状态:执行run()方法,正在运行 阻塞状态:暂时停止执行,把资 ...
- tr
参照http://www.cnblogs.com/djcsch2001/archive/2011/05/08/2040255.html tr -options set1 [set2] ---- set ...
- ios UIView sizeToFit sizeThatFits
UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 50, 0, 0)]; testLabel.backgroundC ...
- PDO预处理
方法:bool PDOStatement::execute ([ array $input_parameters ] ) 1.PDOStatement::execute不使用参数 01)单个绑定值(P ...
- 题目一:打印出所有的 "水仙花数 ",所谓 "水仙花数 "是指一个三位数,其各位数字立方和等于该数本身。例如:153是一个 "水仙花数 ",因为153=1的三次方+5的三次方+3的三次方。
- PHP性能优化工具–xhprof安装
PHP性能优化工具–xhprof安装,这里我先贴出大致的步骤: 1.获取xhprof 2.编译前预处理 3.编译安装 4.配置php.ini 5.查看运行结果 那么下面我们开始安装xhprof工具吧: ...
- C#获取文本文件的编码,自动区分GB2312和UTF8
C# 获取文本文件的编码,自动区分GB2312和UTF8 以下是获取文件编码的一个类 using System; using System.IO; using System.Text; /// < ...
- CMD复制文件夹
CMD复制文件夹 xcopy /E/I/Y "D:\GitHub\WIP\app" "D:\GitHub\WIP_server\html\webshell"
- TypeScript实例
interface Person { firstName: string, lastName: string } function greeter(person: Person) { return p ...