一、需求:
大概的需求就是制作一个简易版的仿OSX系统的计算器,主要功能点有:
- 重置(AC)。
- +-*/运算。
- 数字和小数点的输入。
- 输入左右运算数及运算符之后点击等号可以进行重复计算。
二、界面设计:
先来看看最终的实现效果:
三、HTML结构设计:
页面结构主要分为输入及计算结果展示区域(monitor)和键盘区域(keyboard)。
<div id="calculator" class="calculator-panel">
<div class="monitor">
<div class="display"></div>
</div>
<div class="keyboard">
<div class="left">
<div class="top">
<ul>
<li class="operator largest">AC</li>
</ul>
</div>
<div class="bottom">
<ul>
<li class="number">7</li>
<li class="number">8</li>
<li class="number">9</li>
<li class="number">4</li>
<li class="number">5</li>
<li class="number">6</li>
<li class="number">1</li>
<li class="number">2</li>
<li class="number">3</li>
<li class="number large">0</li>
<li class="number">.</li>
</ul>
</div>
</div>
<div class="right">
<ul>
<li class="operator">÷</li>
<li class="operator">×</li>
<li class="operator">-</li>
<li class="operator">+</li>
<li class="operator">=</li>
</ul>
</div>
</div>
</div>
四:样式设计:
css代码如下:
* {
box-sizing: border-box;
}
.calculator-panel {
width: 200px;
height: 350px;
color: #fff;
font-size: 18px;
background: rgba(100, 98, 96);
}
.calculator-panel ul {
padding: 0;
margin: 0;
list-style: none;
}
.calculator-panel li {
float: left;
width: 50px;
height: 50px;
text-align: center;
line-height: 50px;
border: 1px solid rgba(72, 74, 65);
}
.monitor {
height: 100px;
padding: 60px 10px 0 10px;
font-size: 36px;
}
.monitor > .display {
height: 40px;
line-height: 40px;
text-align: right;
}
.keyboard {
height: 250px;
}
.keyboard > .left,
.keyboard > .right {
float: left;
height: 100%;
}
.keyboard > .left {
width: 150px;
}
.keyboard > .left .operator {
background: rgba(81, 80, 80);
}
.keyboard > .left .operator.largest {
width: 150px;
}
.keyboard > .left .number {
background: rgba(108, 108, 108);
}
.keyboard > .left .number.large {
width: 100px;
}
.keyboard > .right {
width: 50px;
}
.keyboard > .right .operator {
background: rgba(232, 157, 41);
}
五、架构设计:
根据单一职责原则,将功能拆分为4个JS类文件,类之间通过事件通知机制进行通信。
- Monitor.js显示器类,监听显示事件并将内容进行展示。
- Number.js数字输入类,监听数字按键的点击并进行广播。
- Operator.js运算符输入类,监听运算符的点击并进行广播。
- Calculator.js计算器类,监听、广播事件并进行结果的计算。
除此之外,还需要一个EventEmitter.js自定义事件对象,来完成自定义事件的监听和触发。
程序大致的运行流程如下:
- 点击数字按键,Number广播一个数字按键事件。
- Calculator监听数字按键事件,并将输入数字作为左侧运算数。
- 点击运算符,Operator广播一个运算符按键事件。
- Calculator监听运算符按键事件,并将输入作为运算符。
- 点击等号,Operator广播一个运算符按键事件。
- Calculator监听运算符按键事件,并计算结果,广播一个显示内容事件。
- Monitor监听显示内容事件,并将结果进行显示。
六、代码展示:
EventEmitter.js
// 自定义事件监听/触发器
var EventEmitter = {
eventLoops: {}, // 事件队列
subscribe: function (eventName, handler) { // 订阅事件
var handlers = this.eventLoops[eventName];
if (!Array.isArray(handlers)) {
handlers = this.eventLoops[eventName] = [];
}
handlers.push(handler);
},
emit: function (eventName) { // 触发事件
var args = [].slice.call(arguments, 1);
var handlers = this.eventLoops[eventName];
handlers.forEach(function (handler) {
handler(...args);
});
},
remove: function (eventName) { // 移除事件
delete this.eventLoops[eventName];
}
};
Monitor.js
// 监视器构造函数
var Monitor = function Monitor () {
this.node = $(".monitor").find(".display");
};
// 初始化
Monitor.prototype.init = function () {
this.subscribe();
};
// 订阅事件并进行内容显示
Monitor.prototype.subscribe = function () {
EventEmitter.subscribe("calculator.show", (content) => { this.node.text(content); });
};
// 销毁
Monitor.prototype.destroy = function () {
this.node = null;
EventEmitter.remove("calculator.show");
};
Number.js
// Number输入类构造函数
var Number = function Number () {
// 当前输入累加值
this.value = "0";
// dom元素class前缀
this.prefix = "number";
};
// 初始化
Number.prototype.init = function () {
this.subscribe();
// 先执行一次事件,将当前值进行显示
EventEmitter.emit("calculator.show", this.value);
};
// 订阅事件
Number.prototype.subscribe = function () {
var self = this;
// 订阅Number按钮的点击事件
$("." + this.prefix).on("click", function (e) {
var value = $(e.target).text();
self.value = self.value === "0" ? value : self.value + value;
EventEmitter.emit("calculator.show", self.value);
EventEmitter.emit("calculator.number", self.value);
});
// 订阅value重置事件并重置value值
EventEmitter.subscribe("calculator.number.reset", () => { this.value = "0"; });
};
// 销毁
Number.prototype.destroy = function () {
$("." + this.prefix).off("click");
EventEmitter.remove("calculator.number.reset");
};
Operator.js
// 运算符构造函数
var Operator = function Operator () {
this.prefix = "operator";
};
// 初始化
Operator.prototype.init = function () {
this.subscribe();
};
// 订阅事件
Operator.prototype.subscribe = function () {
var self = this;
// 订阅运算符点击事件
$("." + this.prefix).on("click", function (e) {
var value = $(e.target).text();
EventEmitter.emit("calculator.operator", value);
});
};
// 销毁
Operator.prototype.destroy = function () {
$("." + this.prefix).off("click");
};
Calculator.js
// 计算器构造函数,主入口
var Calculator = function Calculator () {
// 左侧数值
this.left = null;
// 右侧数值
this.right = null;
// 运算符
this.operator = null;
// 当前输入模式,"left"表示当前输入的是左侧数值,"right"表示当前输入的右侧数值
this.mode = "left";
// 匹配等号
this.equals = [ "=" ];
// 特殊运算符
this.specialOperators = [ "AC" ];
// 匹配基本运算符
this.basicOperators = [ "÷", "×", "-", "+" ];
// 基本运算符映射
this.basicOperatorMappings = {
"÷": "/",
"×": "*",
"-": "-",
"+": "+"
};
};
// 初始化
Calculator.prototype.init = function () {
this.monitorInstance = new Monitor();
this.numberInstance = new Number();
this.operatorInstance = new Operator();
this.monitorInstance.init();
this.numberInstance.init();
this.operatorInstance.init();
this.subscribe();
};
// 取消订阅事件
Calculator.prototype.unsubscribe = function () {
EventEmitter.remove("calculator.number");
EventEmitter.remove("calculator.operator");
};
// 订阅事件
Calculator.prototype.subscribe = function () {
EventEmitter.subscribe("calculator.number", (number) => { this.onNumberInput(number); });
EventEmitter.subscribe("calculator.operator", (operator) => { this.onOperatorInput(operator); });
};
// 监听数值输入
Calculator.prototype.onNumberInput = function (number) {
// 当前输入的为左侧数值
if (this.mode === "left") this.left = number;
// 当前输入的为右侧数值
if (this.mode === "right") this.right = number;
};
// 监听运算符输入
Calculator.prototype.onOperatorInput = function (operator) {
// 当前输入的是等号,[ "=" ]
if (this.equals.includes(operator)) {
// 排除不合法操作
if (this.operator == null) return;
if (this.left == null && this.right == null) return;
if (this.left == null || this.right == null) return;
this.calcResult();
// 当前输入的基本运算符,[ "÷", "×", "-", "+" ]
} else if (this.basicOperators.includes(operator)) {
// 排除不合法操作
if (this.left == null) return;
// 获取真实操作运算符,防止[ "÷", "×" ]这类非法运算符参与计算
this.operator = this.basicOperatorMappings[operator];
// 切换当前输入为右侧数字
this.mode = "right";
// 重置当前Number的value,以便重新输入右侧数值
EventEmitter.emit("calculator.number.reset");
// 特殊运算符[ "AC" ]
} else if (this.specialOperators.includes(operator)) {
this.reset();
}
};
// 计算结果
Calculator.prototype.calcResult = function () {
// 根据左侧、右侧数值加上运算符计算出结果
// 将结果作为左侧数值继续参与计算
var result = this.left = eval(`${this.left}${this.operator}${this.right}`);
// 切换当前输入为右侧数字
this.mode = "right";
// 重置当前Number的value,以便重新输入右侧数值
EventEmitter.emit("calculator.number.reset");
// 显示计算结果
EventEmitter.emit("calculator.show", result);
};
// 重置
Calculator.prototype.reset = function () {
this.monitorInstance.destroy();
this.numberInstance.destroy();
this.operatorInstance.destroy();
this.unsubscribe();
this.left = null;
this.right = null;
this.operator = null;
this.mode = "left";
this.init();
EventEmitter.emit("calculator.number.reset");
};
七、总结:
核心思路就是Calculator中定义this.left、this.right、this.operator来存储左、右运算数以及当前运算符,点击等号"="时通过将拼接的运算表达式传入eval函数得出计算结果。
完整的项目代码在这里:https://github.com/popelnice/calculator