欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

小哥哥教你撸一个JS计算器

程序员文章站 2022-03-02 18:13:19
...

引言:计算器 Demo 估计会是很多移动端、网页前端新手最佳的第一个上手项目。话说之前学 Android 时从不觉得写个计算器 Demo 会有多难。然而上星期花了几天的时间用原生 JavaScript、CSS、HTML 写了一个计算器 Demo。然而就是这么一个小小的项目还是能让我学到挺多的东西,其中最让我受益的就是明白一个良好的架构对一个软件项目来说是有多么的重要!

项目效果

建议查看效果前去查阅仓库的 README 的 to-do list
在线计算器
GitHub:MyCalculatorDemo

项目详情请看 README 文件,同时欢迎各位 star,fork!
移动端还没有适配,显示存在异常现象

项目背景

其实一开始想要写个计算器 Demo 是源于看完《大话设计模式》的简单工厂模式的那一章节,看完之后就很想实践一下。刚好书中讲解工厂模式的例子举得就是计算器,因为书中是以 c++ 完成工厂模式的构造,于是乎自己就决定按照自己的理解看能不能用 JS 来实现。

项目主要分为 JavaScript 业务逻辑代码和 HTML 界面代码,JavaScript 业务代码主要是 OperationTypeFactory.js 和 SimpleCalculatorIndex.js。OperationTypeFactory.js 是运算类型对象,增加运算类型可在此增加对应运算对象。而 SimpleCalculatorIndex.js 负责运算的业务逻辑。Index.html 是界面代码。

Index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>计算器demo By刘志宇</title>
    <style type="text/css">
        html, body {
            height: 100%;
            -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
        }

        body {
            padding: 0;
            margin: 0;
            background-color: #AFAFAF;
            font: 14px/1.5 Tahoma, "Lucida Grande", Verdana, "Microsoft Yahei", STXihei, hei;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }

        #total {
            width: 100%;
            height: 100%;
            min-width: 300px;
            max-width: 640px;
            margin: 0 auto;
            overflow: hidden;
            background-color: #333;
            box-shadow: 0px 0px 5px 2px #555555;
        }

        header {
            width: 100%;
            height: 25%;
            box-sizing: border-box;
            /*border-bottom: 1px solid #000;*/
            /*box-shadow: 0 0 10px #000 inset,0 0 15px #333;*/
        }

        header input {
            display: block;
            color: #fff;
            text-shadow: 0 2px 1px rgba(0, 0, 0, 0.5);
            height: 50%;
            width: 100%;
            background-color: transparent;
            border: none;
            font-size: 45px;
            font-weight: bolder;
            text-align: right;
            margin: 0 -2%;
        }

        main {
            width: 100%;
            height: 75%;
        }

        main ul {
            list-style: none;
            padding: 0;
            width: 100%;
            height: 100%;
            cursor: pointer;
        }

        main ul li {
            width: 25%;
            height: 20%;
            float: left;
            color: #fff;
            font-size: 26px;
            text-align: center;
            border-left: 1px solid #000;
            border-top: 1px solid #000;
            box-sizing: border-box;
            /*text-shadow: 0 2px 1px rgba(0, 0, 0, 0.2);*/
        }

        main ul li:before {
            display: inline-block;
            content: "";
            height:100%;
            vertical-align:middle;
        }

        .sign{
            background-color: #f5923e;
        }
        .FC{
            border-left: 1px solid transparent;
        }
    </style>
</head>
<body>
<div id="total">
    <header>
        <input type="text" id="sum" value="" readonly>
        <input type="text" id="content" value="0" readonly>
    </header>
    <main>
        <ul id="list">
            <li class="sign FC" onclick="Main.onreset()">AC</li>
            <li class="sign" onclick="Main.inClick('d')">←</li>
            <li class="sign" onclick="compute('%')">mod</li>
            <li class="sign" onclick="compute('+')">+</li>
            <li class="num FC" onclick="Main.inClick(7)">7</li>
            <li class="num" onclick="Main.inClick(8)">8</li>
            <li class="num" onclick="Main.inClick(9)">9</li>
            <li class="sign" onclick="compute('-')">-</li>
            <li class="num FC" onclick="Main.inClick(4)">4</li>
            <li class="num" onclick="Main.inClick(5)">5</li>
            <li class="num" onclick="Main.inClick(6)">6</li>
            <li class="sign" onclick="compute('X')">×</li>
            <li class="num FC" onclick="Main.inClick(1)">1</li>
            <li class="num" onclick="Main.inClick(2)">2</li>
            <li class="num" onclick="Main.inClick(3)">3</li>
            <li class="sign" onclick="compute('/')">÷</li>
            <li class="num FC" onclick="Main.inClick('.')">.</li>
            <li class="num" onclick="Main.inClick(0)">0</li>
            <li class="num" onclick="Main.inClick('PI')">PI</li>   <!--Math.PI-->
            <li class="sign" onclick="Main.display()">=</li>

            <!--<li class="sign" onclick="compute('^')">x³</li>-->
            <!--<li class="sign" onclick="compute('#')">√</li>-->
            <!--<li class="sign" onclick="compute('sin')">sin</li>-->
            <!--<li class="sign" onclick="compute('cos')">cos</li>-->
        </ul>
    </main>
</div>
<script src="scripts/OperationTypeFactory.js"></script>
<script src="scripts/SimpleCalculatorIndex.js"></script>
<script>
    window.onload=function () {
        alert("使用提示:使用时请完整输入运算表达式,运算结果将不保存!");
    }
</script>
</body>
</html>

界面部分<body>标签内主要分为计算器显示部分<header>和计算机输入部分<main>。其中按键界面是由<ul>无序列表构成。具体可看上面代码,这里比较简单,不一一细讲。

OperationTypeFactory.js

'use strict';
//运算原型
var Operation = {
    first: 0,
    second: 0,
    result: 0
};

//各种运算对象
var Add = {
    get: function (num1, num2) {
        var i = accurate(num1, num2);
        Operation.result = (num1 * i + num2 * i) / i;
    },
    __proto__: Operation
};
var Sub = {
    get: function (num1, num2) {
        var i = accurate(num1, num2);
        Operation.result = (num1 * i - num2 * i) / i;
    },
    __proto__: Operation
};

var Multi = {
    get: function (num1, num2) {
        var i = accurate(num1, num2);
        Operation.result = ((num1 * i) * (num2 * i)) / Math.pow(i, 2);
    },
    __proto__: Operation
};

var Div = {
    get: function (num1, num2) {
        if (num1 === 0) {
            alert("被除数不能为0");
            return;
        }
        var i = accurate(num1, num2);
        Operation.result = ((num1 * i) / (num2 * i));
    },
    __proto__: Operation
};

var Pow = {
    get: function (num1, num2) {
        Operation.result = Math.pow(num1, num2);
    },
    __proto__: Operation
};

var Sqrt = {
    get: function (num1, num2) {
        Operation.result = Math.sqrt(num1);
    },
    __proto__: Operation
};

var Sin = {
    get: function (num1, num2) {
        var a = (Math.PI / 180) * num1;
        Operation.result = Math.sin(a);
    },
    __proto__: Operation
};

var Cos = {
    get: function (num1, num2) {
        var b = (Math.PI / 180) * num1;
        Operation.result = Math.cos(b);
    },
    __proto__: Operation
};

var Mod = {
    get: function (num1, num2) {
        Operation.result = num1 % num2;
    },
    __proto__: Operation
};

//运算对象工厂
var operationFactory = {
    produce: function (math) {
        switch (math) {
            case '+':
                Main.computer = Add;
                break;
            case '-':
                Main.computer = Sub;
                break;
            case 'X':
                Main.computer = Multi;
                break;
            case '/':
                Main.computer = Div;
                break;
            case '^':
                Main.computer = Pow;
                break;
            case '#':
                Main.computer = Sqrt;
                break;
            case 'sin':
                Main.computer = Sin;
                break;
            case 'cos':
                Main.computer = Cos;
                break;
            case '%':
                Main.computer = Mod;
                break;
        }
    },
    __proto__: Operation
};

//解决浮点数运算不精确问题
function accurate(a1, a2) {
    var n1 = 0, n2 = 0, m = 1;
    try{
        n1 = a1.toString().split(".")[1].length;
    }catch (e){

    }
    try {
        n2 = a2.toString().split(".")[1].length;
    }catch (e){

    }
    m = Math.pow(10, Math.max(n1, n2));
    return m;
}

Operation 对象保存的是参与运算的数值,而它的子类是各种各样的运算对象,其 get 函数逻辑各不相同。最后根据输入的符号新建一个运算对象。

SimpleCalculatorIndex.js

'use strict';
//显示台对象
var Main = {
    isFirst: true,                                  //用以判断当前输入环境是Operation.first还是Operation.second
    temp: 0,                                        //输入数字的缓存
    count: 0,                                       //统计输入数字的次数
    description: document.getElementById("sum"),    //用来显示运算过程
    out: document.getElementById("content"),        //用来显示运算结果
    formula: "",                                    //用来记录运算表达式,以便输出到运算过程显示框
    computer: 0,                                    //运算实体对象
    subCount: 0,                                    //记录减号按键次数
    addCount: 0,                                    //记录加号按键次数
    multiCount: 0,                                  //记录乘号按键次数
    divCount: 0,                                    //记录除号按键次数
    modCount: 0,                                    //记录求模按键次数
    subLocation: -1,                                //记录运算公式字符串中运算符号位置
    dotAddress: -1,                                 //记录小数点在数值字符串中的位置
    dotFlag: false,                                 //用以判断小数点按键是否按下
    dotLocation: -1,                                //永久保存subLocation
    deleteTemp: 0,                                  //永久保存存入Operation.first的temp值
    deleteCount: 0,                                 //永久保存存入Operation.first的count值
    deleteDotAddress: -1,                           //永久保存存入Operation.first的dotAddress值

    //inClick 函数接受计算器的数字输入
    inClick: function (number) {
        if (number === 'd') {
            var last = this.formula.charAt(this.formula.length - 1);
            var fl = this.formula.length;
            if (last === '.' || last === '+' || last === '-' || last === 'X' || last === '/' || last === '%') {
                switch (last) {
                    case '.':
                        this.dotAddress = -1;
                        this.dotFlag = false;
                        break;
                    case '+':
                        this.addCount--;
                        this.isFirst = true;
                        this.temp = this.deleteTemp;
                        this.count = this.deleteCount;
                        this.dotAddress = this.deleteDotAddress;
                        break;
                    case '-':
                        this.subCount--;
                        if (fl === this.dotLocation + 1) {
                            this.isFirst = true;
                            this.temp = this.deleteTemp;
                            this.count = this.deleteCount;
                            this.dotAddress = this.deleteDotAddress;
                        }
                        break;
                    case 'X':
                        this.multiCount--;
                        this.isFirst = true;
                        this.temp = this.deleteTemp;
                        this.count = this.deleteCount;
                        this.dotAddress = this.deleteDotAddress;
                        break;
                    case '/':
                        this.divCount--;
                        this.isFirst = true;
                        this.temp = this.deleteTemp;
                        this.count = this.deleteCount;
                        this.dotAddress = this.deleteDotAddress;
                        break;
                    case '%':
                        this.modCount--;
                        this.isFirst = true;
                        this.temp = this.deleteTemp;
                        this.count = this.deleteCount;
                        this.dotAddress = this.deleteDotAddress;
                        break;
                }
                this.formula = this.formula.substring(0, this.formula.length - 1);
                this.description.value = this.formula;
                return;
            }

            this.formula = this.formula.substring(0, this.formula.length - 1);
            this.description.value = this.formula;

            var f = this.temp % 10;
            f /= 10;
            this.temp = this.temp / 10 - f;
            --this.count;
            if (this.isFirst) {
                Operation.first = this.temp;
                this.subLocation = this.formula.length;
                this.dotLocation = this.subLocation;
            } else {
                Operation.second = this.temp;
            }
        } else {
            this.formula += number;
            this.description.value = this.formula;
        }

        if (number === 'PI') {
            if (this.isFirst) {
                Operation.first = Math.PI;
                this.subLocation = this.formula.length;            //保证记录的是第一个运算数的后面那个运算符
                this.dotLocation = this.subLocation;
            } else {
                Operation.second = Math.PI;
            }
        }

        if (number === '.') {
            this.dotAddress = this.count;
            this.dotFlag = true;
        }

        if (typeof number === "number") {
            if (this.count === 0) {
                this.temp += number;
            } else {
                this.temp = this.temp * 10 + number;
            }
            this.count++;

            if (this.isFirst) {
                Operation.first = this.temp;
                this.subLocation = this.formula.length;
                this.dotLocation = this.subLocation;
                this.deleteTemp = this.temp;
                this.deleteCount = this.count;
                this.deleteDotAddress = this.dotAddress;
            } else {
                Operation.second = this.temp;
            }
        }
    },

    //该函数接受运算符号的输入
    getOperation: function (math) {
        //如果Operation.first是小数的话,在输入运算符号之前进行小数化
        if (this.dotFlag) {
            var mi = this.count - this.dotAddress;
            for (let i = 0; i < mi; i++) {
                this.temp /= 10;
            }
            if (this.isFirst) {
                Operation.first = this.temp;
            }
            this.dotFlag = !this.dotFlag;
        }
        this.formula += math;
        this.description.value = this.formula;


        //此时记录输入各个运算符号按键的次数
        switch (math) {
            case '+':
                Main.addCount++;
                break;
            case '-':
                Main.subCount++;
                break;
            case 'X':
                Main.multiCount++;
                break;
            case '/':
                Main.divCount++;
                break;
            case '%':
                Main.modCount++;
                break;
        }

        //根据传入的运算符号进行输入环境的切换
        if (math === '+' || math === 'X' || math === '%' || math === '^' || math === '/') {
            this.isFirst = !this.isFirst;
        } else if (math === '-') {
            if (this.formula.charAt(this.subLocation) === '-') {
                this.isFirst = !this.isFirst;
                this.subLocation = -2;         //防止减号运算和第二个运算数也为负数的矛盾
            }
        }

        this.count = 0;
        this.temp = 0;
        this.dotAddress = -1;
    },

    //该函数负责运算结果的计算以及显示
    display: function () {
        //如果第二个运算数为小数的话,在此进行小数化
        if (this.dotFlag) {
            var m = this.count - this.dotAddress;
            for (let i = 0; i < m; i++) {
                this.temp /= 10;
            }
            if (this.isFirst) {
                Operation.first = this.temp;
            } else {
                Operation.second = this.temp;
            }
            this.dotFlag = !this.dotFlag;
        }


        //计算之前首先根据各运算符按键次数判断运算逻辑
        if (this.subCount === 1) {
            if (this.addCount != 0 || this.multiCount != 0 || this.divCount != 0 || this.modCount != 0) {
                if (this.formula.charAt(0) === '-') {
                    Operation.first = Operation.first * (-1);
                } else {
                    Operation.second = Operation.second * (-1);
                    if (this.addCount === 1) {
                        operationFactory.produce('+');
                    }
                    if (this.multiCount === 1) {
                        operationFactory.produce('X');
                    }
                    if (this.divCount === 1) {
                        operationFactory.produce('/');
                    }
                    if (this.modCount === 1) {
                        operationFactory.produce('%');
                    }
                }
            } else {
                operationFactory.produce('-');
            }
        } else if (this.subCount === 2) {
            if (this.addCount != 0 || this.multiCount != 0 || this.divCount != 0 || this.modCount != 0) {
                Operation.first *= (-1);
                Operation.second *= (-1);
                if (this.addCount === 1) {
                    operationFactory.produce('+');
                }
                if (this.multiCount === 1) {
                    operationFactory.produce('X');
                }
                if (this.divCount === 1) {
                    operationFactory.produce('/');
                }
                if (this.modCount === 1) {
                    operationFactory.produce('%');
                }
            } else {
                if (this.formula.charAt(0) === '-') {
                    Operation.first *= (-1);
                } else {
                    Operation.second *= (-1);
                }
            }
        } else if (this.subCount === 3) {
            Operation.first *= (-1);
            Operation.second *= (-1);
        }


        this.formula += '=';
        this.description.value = this.formula;
        this.computer.get(Operation.first, Operation.second);
        this.out.value = Operation.result;
        this.count = 0;
        this.temp = 0;
        this.formula = "";
        this.subLocation = -1;
        this.addCount = 0;
        this.subCount = 0;
        this.multiCount = 0;
        this.divCount = 0;
        this.modCount = 0;
        this.deleteCount = 0;
        this.deleteTemp = 0;
        this.dotLocation = -1;
        if (this.isFirst === false) {
            this.isFirst = !this.isFirst;
        }

    },
    onreset: function () {
        Operation.first = 0;
        Operation.second = 0;
        Operation.result = 0;
        this.isFirst = true;
        this.count = 0;
        this.temp = 0;
        this.formula = "";
        this.out.value = 0;
        this.description.value = "";
        this.dotFlag = false;
        this.subLocation = -1;
        this.dotAddress = -1;
        this.subCount = 0;
        this.addCount = 0;
        this.multiCount = 0;
        this.divCount = 0;
        this.modCount = 0;
        this.deleteCount = 0;
        this.deleteTemp = 0;
        this.dotLocation = -1;
    }
};

//同时向显示台对象和运算对象工厂传入运算类型参数
function compute(math) {
    Main.getOperation(math);
    operationFactory.produce(math);
}

最重要的是此计算业务逻辑代码。在这里处理输入的数字和符号去获得结果并显示出来。Operation.first 和 Operation.second 是进行运算的两个运算值,Operation.result 是运算的最终结果。而 Operation.first 和 Operation.second 是由传入的数字按键直接赋值,因此未进行处理前都是正整数。而处理小数点、负号、退位按键都是在此正整数的基础上操作而来,这也是此项目中的重点难点。我这里大概讲一下处理这三个难点的逻辑思路:小数点是每次输入完数字之后按下操作运算符号按键(除数字按键之外的按键)时根据之前记录的小数点的位置,将正整数进行一系列数学运算之后修正为正确的数值;负号处理是在最后按键等号按键时根据此前记录各个运算按键的按下次数去判断按下的负号是代表减法运算的运算符号、还是代表为第一个运算值的负号或者第二个运算值的负号、或者是代表两个运算值的负号、还有可能是两个负数进行减法运算。判断完毕后再对此前的两个运算值进行修正;退位是在判断输入按键为退位按键时根据当时按下退位按键时运算表达式的最后一个字符进行相对应处理。比如运算表达式最后一个为数字字符,则简单进行数值修正。而如果运算表达式最后一个为非数字字符则要进行对应的逻辑操作,比如切换输入环境。

由于这三个难题的处理步骤处在不同操作中,耦合性较低。因此也较容易理解业务逻辑,同时上面也标注上对应的注释,方便大家理解。

最后

这是我第一次较为完整的撸完一个相对简单的项目,同时写这个计算器 Demo 之前我也没有参考过其他优秀的计算器项目的源码,因此肯定还存在不合理的地方,欢迎各位大牛提出宝贵意见。同时如果各位阅读代码过程中有疑问可以提出来,我会认真回答各位问题,这也是帮助我提高编程能力的途径之一。

最后是广告时间,我的原创博文将同步更新在三大平台上,欢迎大家点击阅读!谢谢

刘志宇的新天地

简书

稀土掘金