引言
这是一篇很棒的文章,在这里你可以学习如何从零做出一款计算器。我们希望你使用 JavaScript 开发并且思考怎么构建一款计算器, 如何编写代码,以及最后,如何整理自己的代码。
在这篇文章结束,你会得到一款和 iPhone 计算器功能一样的计算器(除了 +/- 和百分比功能外)。
前置条件
在你开始本节课程前,请确保你对 JavaScript 有一个不错的了解。最起码,你需要知道以下事情:
- If/else 分支
- For 循环
- JavaScript 函数
- 箭头函数
&&
和||
操作符- 如何使用
textContent
属性修改文本 - 如何使用事件代理模式添加事件
开始之前
我建议你在开始课程之前自己尝试下自己开发计算器。这是一个很好的锻炼,因为你会训练自己像开发人员一样思考。
一旦你尝试了一小时,再回来上这节课(不管你是成功还是失败。当年尝试过,思考过,这会帮助你在更短的时间内吸收本节课的内容)。
就这样,我们先来了解下计算器的工作原理。如果对Python有兴趣,想了解更多的Python以及AIoT知识,解决测试问题,以及入门指导,帮你解决学习Python中遇到的困惑,我们这里有技术高手。如果你正在找工作或者刚刚学校出来,又或者已经工作但是经常觉得难点很多,觉得自己Python方面学的不够精想要继续学习的,想转行怕学不会的, 都可以加入我们,可领取最新Python大厂面试资料和Python爬虫、人工智能、学习资料!VX【pydby01】暗号CSDN
构建计算器
首先,我们想要建立计算器。
这个计算机包含两个部分:显示屏和键盘。
0 …
我们使用 CSS Grid 去制作键盘部分,因为他们是类似网格的格式进行排列的。这里已经在启动文件中完成了,你可以在以下地址找到启动文件此处.
.calculator__keys { display: grid; /* other necessary CSS */ }
为了帮助我们区分操作符,小数点,清除符号以及等号,我们将设置一个data-action
属性用来描述他们的功能。
监听键盘点击
当一个人拿着几个计算器,他会做五种事情,他们可以点击:
- 一个数字键(0-9)
- 一个操作键 (+,-,×,÷)
- 小数点键
- 等号键
- 清除键
构建这个计算器的第一步是能够监听所有(1)的按键,确定(2)被按下时候的类型。在这个案例中,我们可以使用事件代理模式去监听,因为所有的按键都是.calculator__keys
的孩子。
const calculator = document.querySelector(‘.calculator’)const keys = calculator.querySelector(‘.calculator__keys’)keys.addEventListener(‘click’, e => { if (e.target.matches(‘button’)) { // Do something }})
接下来,我们利用data-action
属性去确定点击按键的类型。
const key = e.targetconst action = key.dataset.action
如果按键没有data-action
属性,那么它一定是一个数字键。
if (!action) {console.log('number key!')}
如果这个按键有data-action
,它的值是add
,subtract
,multiply
或者divide
,我们就可以知道这是一个操作按键。
if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') {console.log('operator key!')}
如果这个按键的data-action
属性是decimal
,我们就可以知道使用者点击了小数点键。
按照同样的思路,如果键的data-action
是clear
,我们知道用户点击了清除(写着 AC 的那个)键。如果键的data-action
是calculate
,我们知道用户点击了等于键。
if (action === 'decimal') {console.log('decimal key!')}if (action === 'clear') {console.log('clear key!')}if (action === 'calculate') {console.log('equal key!')}
在这里,你可以使用console.log
方法,来响应每个按键的事件。
开始构建 happy path
让我们思考一下,一个普通人拿到一个计算器之后,会做什么呢?这个普通人会做什么的问题被称作 happy path。
这个普通人我们就称作 Mary 吧。
当 Mary拿起计算器时,她可能会点击任何一个按键:
- 一个数字键(0-9)
- 一个操作键 (+,-,×,÷)
- 小数点键
- 等号键
- 清除键
一下子要思考五种按键可以能不太容易,所以让我们一步一步来。
当使用者按下数字键
如果计算器显示 0(默认数字),此时,目标数字需要替换这个 0。
如果计算器显示的是非零数字,那么目标数字就需要在显示的数字后面添加上。
现在,我们需要知道两件事情:
- 当前被点击的按键的数字。
- 当前显示的数字。
我们可以通过textContent
和点击按键的.calculator__display
分别获取到这两个值。
const display = document.querySelector('.calculator__display')keys.addEventListener('click', e => {if (e.target.matches('button')) {const key = e.targetconst action = key.dataset.actionconst keyContent = key.textContentconst displayedNum = display.textContent// ...}})
如果计算器显示0,我们需要用点击按键的数字替换计算器显示屏的数字。我们可以通过显示屏的textContent
属性进行替换。
if (!action) {if (displayedNum === '0') {display.textContent = keyContent}}
如果计算器显示的是非零数字,我们需要在当前显示的数字后面追加点击键的数字。要追加一个数字,我们就需要一个连接字符串。
if (!action) {if (displayedNum === '0') {display.textContent = keyContent} else {display.textContent = displayedNum + keyContent}}
这时,Mary 可能会点击其中一个按键:
- 小数点键
- 操作符键
让我们告诉 Mary 点击一下小数点键吧。
当使用者点击小数点键时
当 Mary 点击了小数点键之后,小数点就需要出现在显示屏上。如果 Mary 在敲击小数键后敲击任何数字,那么数字也应该添加在显示屏上。
为了实现上述效果,我们需要将.
添加到已经显示的数字后面。如果对Python有兴趣,想了解更多的Python以及AIoT知识,解决测试问题,以及入门指导,帮你解决学习Python中遇到的困惑,我们这里有技术高手。如果你正在找工作或者刚刚学校出来,又或者已经工作但是经常觉得难点很多,觉得自己Python方面学的不够精想要继续学习的,想转行怕学不会的, 都可以加入我们,可领取最新Python大厂面试资料和Python爬虫、人工智能、学习资料!VX【pydby01】暗号CSDN
if (action === 'decimal') {display.textContent = displayedNum + '.'}
接下来,我们可以让 Mary 继续点击计算器的操作按键继续她的计算。
当使用者点击操作按钮
如果 Mary 点击操作按键,这个操作符需要被高亮,这样的话 Mary 就知道了这个操作符是激活的。
为了实现这个功能,我们给操作符按钮添加一个名字叫is-depressed
的类名。
if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') {key.classList.add('is-depressed')}
一旦 Mary 按下了一个操作键,她将会点击另外的数字键。
当使用者在点击了操作键后点击了数字键
当 Mary 再次点击了数字键,之前显示的数字应该被替换成新的数组。操作键也应该被解除“被点击”的状态。
我们可以使用forEach
循环遍历所有的按键,去移除is-depressed
类:
keys.addEventListener('click', e => {if (e.target.matches('button')) {const key = e.target// ...// Remove .is-depressed class from all keysArray.from(key.parentNode.children).forEach(k => k.classList.remove('is-depressed'))}})
接下来,我们想要把显示的内容更新为之前点击过的按键。在我们做这件事之前,我们需要判断之前的按键是否是一个操作键。
我们可以通过自定义属性来实现。让我们定义一个自定义属性data-previous-key-type
。
const calculator = document.querySelector('.calculator')// ...keys.addEventListener('click', e => {if (e.target.matches('button')) {// ...if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') {key.classList.add('is-depressed')// Add custom attributecalculator.dataset.previousKeyType = 'operator'}}})
If thepreviousKeyType
is an operator, we want to replace the displayed number with clicked number.
如果previousKeyType
是一个操作符,我们希望可以用当前点击的数字替换当前显示的数字。
const previousKeyType = calculator.dataset.previousKeyTypeif (!action) {if (displayedNum === '0' || previousKeyType === 'operator') {display.textContent = keyContent} else {display.textContent = displayedNum + keyContent}}
接下来让我们告诉 Mary 点击等号键来完成她的计算。
当使用者点击等号键时
当 Mary 点击等号键,计算器应该根据三个值计算一个结果:
- 第一个输入计算器中的数字
- 操作符
- 第二个输入计算器中的数字
在计算之后,结果会替换当前已显示的值出现在屏幕上。
这里我们只知道第二个数字是当前已经显示的数字。
if (action === 'calculate') {const secondValue = displayedNum// ...}
为了获取第一个数字,我们需要储存之前在计算器上被我们已经清除了的值。我们可以添加一个自定义的属性,在我们点击操作键是储存第一个值。
获取操作符,我们可以使用同样的方法。
if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') {// ...calculator.dataset.firstValue = displayedNumcalculator.dataset.operator = action}
一旦我们得到了三个我们需要的值,接下来我们就可以进行计算。最终,我们需要实现这样的代码:
if (action === 'calculate') {const firstValue = calculator.dataset.firstValueconst operator = calculator.dataset.operatorconst secondValue = displayedNum
接下来我们需要构建一个calculate
方法。它需要接收第一个数字,操作符和第二个数字三个参数。
const calculate = (n1, operator, n2) => {// Perform calculation and return calculated value}
如果操作符是add
,我们希望两个数字可以相加在一起。如果操作符是subtract
,则希望两个数字相减,其余的操作符也是如此。
const calculate = (n1, operator, n2) => {let result = ''if (operator === 'add') {result = n1 + n2} else if (operator === 'subtract') {result = n1 - n2} else if (operator === 'multiply') {result = n1 * n2} else if (operator === 'divide') {result = n1 / n2}
请记住现在的第一个数字
和第二个数字
都是字符串。如果你进行字符串相加的话,一会把它们连在一起 (1 + 1 = 11
)。
所以在计算结果之前,我们需要将字符串类型转换成数字类型。我们可以使用parseInt
和parseFloat
两个方法来实现。
parseInt
converts a string into aninteger.parseFloat
converts a string into afloat(this means a number with decimal places).
对于计算器来说,我们需要浮点数。
const calculate = (n1, operator, n2) => {let result = ''if (operator === 'add') {result = parseFloat(n1) + parseFloat(n2)} else if (operator === 'subtract') {result = parseFloat(n1) - parseFloat(n2)} else if (operator === 'multiply') {result = parseFloat(n1) * parseFloat(n2)} else if (operator === 'divide') {result = parseFloat(n1) / parseFloat(n2)}
你可以通过这个链接获取源代码(往下滚动,在方框里输入你的邮箱地址,我就会把源代码直接发到你的邮箱里)。
边缘的测试用例
如果需要构建一款足够健壮的计算器,你需要使你的计算器能够适应各种奇怪的输入。
因此,你需要想象有一个破坏者,他会尝试按照错误的点击顺序来破坏你的计算器。我们就把这个破坏者叫做 Tim 吧。
Tim 可以按照任何的方式点击这些按键:
- 数字键
- 运算符键
- 小数点键
- 等号键
- 清除键
当 Tim 点击小数点键的时候会发生什么呢
如果在 Tim 点击小数点键之前已经有小数点显示在屏幕上了,那么他点击之后将什么都不会发生。
我们可以利用includes
方法检查是否已经包含.
。
includes
方法会检查字符串是否匹配。如果找到一个字符串,它返回 “true”;如果没有,它返回 “false”。
注:includes
区分大小写。
// Example of how includes work.const string = 'The hamburgers taste pretty good!'const hasExclaimation = string.includes('!')console.log(hasExclaimation) // true
检查字符串中是否包含小数点的方法如下:
// Do nothing if string has a dotif (!displayedNum.includes('.')) {display.textContent = displayedNum + '.'}
接下来,如果 Tim 在点击任何操作键之后点击了小数点键,那么应该显示为0.
。
我们需要知道上一个按键是否是操作符键。 我们可以通过上节课设置的自定义属性data-previous-key-type
来判断。
当然data-previous-key-type
还没有完成,为了判断previousKeyType
是否是操作符,我们还需要在每次点击按键时更新previousKeyType
。
if (!action) {// ...calculator.dataset.previousKey = 'number'}if (action === 'decimal') {// ...calculator.dataset.previousKey = 'decimal'}if (action === 'clear') {// ...calculator.dataset.previousKeyType = 'clear'}
现在,我们正确的获取了previousKeyType
,我们可以使用它来判断上一次按键是否是操作符键。
if (action === 'decimal') {if (!displayedNum.includes('.')) {display.textContent = displayedNum + '.'} else if (previousKeyType === 'operator') {display.textContent = '0.'}
当 Tim 点击操作符键会发生什么
首先第一种情况,如果 Tim 首先点击了操作键,那么按键就会高亮。(We’ve already covered for this edge case, but how” />
第二种情况,如果 Tim 多次点击同样的操作键,应该什么都不会发生。(我们也已经涵盖了这种边缘情况)。
注:如果想要提供更好的用户体验,你可以通过 CSS 来让操作者的反复点击得到反馈。 我们不在这里实现,你可以将这个功能当作一次挑战,看看如何实现。
情况,如果 Tim 在点击一个操作键之后又点击了另外一个操作键,那么第一个按的操作键会被解除点击状态,第二次按的操作键应该被设置成按压状态。(我们也覆盖了这种情况,但如何实现的?)
第四种情况,如果 Tim 点击了一个数字键,一个操作键和另外一个操作键,这种情况下,应当直接显示计算之后的结果。
这就意味着在firstValue
,operator
和secondValue
三个参数存在时,我们需要调用calculate
方法。
if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') {const firstValue = calculator.dataset.firstValueconst operator = calculator.dataset.operatorconst secondValue = displayedNum// Note: It's sufficient to check for firstValue and operator because secondValue always existsif (firstValue && operator) {display.textContent = calculate(firstValue, operator, secondValue)}
尽管我们在第二次点击操作键的时候我们可以得到一个计算的值,但这里依然有一个bug存在————额外点击操作键会计算出一个不应该的值。
为了防止计算器在后续点击操作键时进行计算,我们需要检查previousKeyType
是否是一个操作键。如果是,我们不执行计算。
if (firstValue &&operator &&previousKeyType !== 'operator') {display.textContent = calculate(firstValue, operator, secondValue)}
第五种情况,在点击操作键之后计算出一个数字之后,如果 Tim 又点击了一下数字键,接着又按了下操作键,操作键应该继续之前的结果进行计算,就像这样:8 - 1 = 7
,7 - 2 = 5
,5 - 3 = 2
。
现在,我们的计算器不能进行连续计算。第二个计算值是错误的。我们的计算结果是这样的:99 - 1 = 98
,98 - 1 = 0
。99 - 1 = 98
,98 - 1 = 0
。
第二个值是计算错误的,因为我们把错误的值输入了calculate
函数。让我们通过几张图片来了解我们的代码是怎么做的。
理解 calculate 方法
首先,我们告诉使用者输入一个数字 99,此时,计算器没有储存任何值。
接着,我们让使用者点击一下减号键,在他点击减号键之后,我们设置firstValue
为 99,同样的设置operator
为subtract
。
第三步,假设用户这次输入的数字是 1,此时,将显示的数字改成1,但是我们的firstValue
,operator
和secondValue
保持不变。
第四步,用户再次点击减号键。就在他们点击减法后,在计算结果之前,我们设置secondValue
作为显示的数字。
第五步,我们用firstValue
99,operator
减号以及secondValue
1进行计算,得到结果 98。
计算出结果后,我们将显示设置为结果。然后,我们设置operator
为减法,firstValue
为之前显示的数字。
好吧,这是非常错误的!如果我们想继续计算,我们需要用计算值更新firstValue
。如果我们想继续计算,我们需要用计算值更新firstValue
。
const firstValue = calculator.dataset.firstValueconst operator = calculator.dataset.operatorconst secondValue = displayedNumif (firstValue &&operator &&previousKeyType !== 'operator') {const calcValue = calculate(firstValue, operator, secondValue)display.textContent = calcValue// Update calculated value as firstValuecalculator.dataset.firstValue = calcValue} else {// If there are no calculations, set displayedNum as the firstValuecalculator.dataset.firstValue = displayedNum}
修改之后,现在通过操作键进行的连续计算应该是正确的。
Tim 点击等号键时候发生了什么?
第一种情况,Tim 在点击等号前没点击过任何操作键,那么什么都不会发生。
我们知道,如果firstValue
没有设置为数字,也就代表操作键还没有被点击。我们可以利用这个点来防止等号进行计算。
if (action === 'calculate') {const firstValue = calculator.dataset.firstValueconst operator = calculator.dataset.operatorconst secondValue = displayedNumif (firstValue) {display.textContent = calculate(firstValue, operator, secondValue)}
第二种情况,如果 Tim 输入了一个数字,接着又按下了操作键,随后又按了等号键。计算器计算的结果应该是这样:
2 + =
—>2 + 2 = 4
2 - =
—>2 - 2 = 0
2 × =
—>2 × 2 = 4
2 ÷ =
—>2 ÷ 2 = 1
我们已经处理了这种奇怪的情况,你知道为什么吗?
第三种情况,如果 Tim 在一次计算完成之后点击了等号键,应该进行另外一次计算,例如这样:
- Tim 点击了
5 - 1
- Tim 点击了等号,计算的值是
5 - 1 = 4
- Tim 点击了等号,计算的值是
4 - 1 = 3
- Tim 点击了等号,计算的值是
3 - 1 = 2
- Tim 点击了等号,计算的值是
2 - 1 = 1
- Tim 点击了等号,计算的值是
1 - 1 = 0
不幸的是,我们的把这个计算弄乱了,下面是我们计算的结果:
- Tim 输入
5 - 1
- Tim 点击等号,计算结果是
4
- Tim 再点击等号,计算结果是
1
修改计算
首先让我们的用户点击数字 5,,此时计算器中没有任何被定义过的东西。
第二步,让用户点击减号键,再点击减号键之后,我们设置firstValue
为 5,同时设置operator
为减号。
第三步,让用户输入第二个值,假设是数字 1。此时,显示的数字应该被更新为1,但是我们的firstValue,
operator和
secondValue`是保持不变的。
第四步,用户点击等号键。紧接着用户点击了等号,但是在计算之前,我们设置secondValue
为displayedNum
。
第四,用户点击等号键后,我们设置secondValue
为displayNum
。就在他们点击等号之后,但在计算之前,我们设置secondValue
为displayedNum
。
第五,计算器计算5-1
并且得到结果4
。得到结果并将显示的数字更新。firstValue
和operator
会在下一次计算中使用,因为我们没有更新它们。
第六,当用户再次点击等号键,我们在计算之前把secondValue
设置成displayNum
。
这里有一个问题。
我们要的不是 “secondValue”,而是设置 “firstValue “为显示的数字。
if (action === 'calculate') {let firstValue = calculator.dataset.firstValueconst operator = calculator.dataset.operatorconst secondValue = displayedNumif (firstValue) {if (previousKeyType === 'calculate') {firstValue = displayedNum}display.textContent = calculate(firstValue, operator, secondValue)}
我们可能也想把上一次计算的secondValue
带到下一次计算当中。为了做到这个功能,我们需要利用另外的自定义属性来存储它。让我们来定义一个叫modValue
的属性。
if (action === 'calculate') {let firstValue = calculator.dataset.firstValueconst operator = calculator.dataset.operatorconst secondValue = displayedNumif (firstValue) {if (previousKeyType === 'calculate') {firstValue = displayedNum}display.textContent = calculate(firstValue, operator, secondValue)}
如果previousKeyType
是calculate
,我可以使用calculator.dataset.modValue
作为secondValue
。知道这个的话,我们就可以进行计算
if (firstValue) {if (previousKeyType === 'calculate') {firstValue = displayedNumsecondValue = calculator.dataset.modValue}
这样一来,当连续点击等号键时,我们就有了正确的计算方法。
回到等号键
第四,如果 Tim 在计算器键后按下小数键或数字键,则应分别用0.
或新数字代替显示。
在这里,我们不只检查previousKeyType
是否是operator
,还需要检查是否是calculate
。
if (!action) {if (displayedNum === '0' ||previousKeyType === 'operator' ||previousKeyType === 'calculate') {display.textContent = keyContent} else {display.textContent = displayedNum + keyContent}calculator.dataset.previousKeyType = 'number'}if (action === 'decimal') {if (!displayedNum.includes('.')) {display.textContent = displayedNum + '.'} else if (previousKeyType === 'operator' ||previousKeyType === 'calculate') {display.textContent = '0.'}
第五,如果 Tim 再点击等号之后又点击了操作键,计算器则不应该进行计算。
为此,我们在用操作键进行计算之前,先检查previousKeyType
是否为calculate
。
if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') {// ...if (firstValue &&operator &&previousKeyType !== 'operator' &&previousKeyType !== 'calculate') {const calcValue = calculate(firstValue, operator, secondValue)display.textContent = calcValuecalculator.dataset.firstValue = calcValue} else {calculator.dataset.firstValue = displayedNum}
清除键有两种用法:
- 全部清除(用 “AC “表示)清除所有的东西,并将计算器恢复到初始状态。
- 清除输入(用 “CE “表示)清除当前的输入。它将以前的数字保留在内存中。
当计算器处于默认状态时,应该显示 “AC”。
首先,如果 Tim 点击了一个键(除了清ad除键之外的任何键),AC
应该被改成CE
。
我们通过检查data-action
是不是clear
来判断,如果不是clear
,我们找到清除按钮,并改变textContent
。
if (action !== 'clear') {const clearButton = calculator.querySelector('[data-action=clear]')clearButton.textContent = 'CE'}
接下来,如果 Tim 点击CE
,显示的数字应该为0。与此同时,CE
应该改为AC
。所以 Tim 可以将计算器重置到初始状态。
if (action === 'clear') {display.textContent = 0key.textContent = 'AC'calculator.dataset.previousKeyType = 'clear'}
第三,如果 Tim 点击了AC
,重置了计算器的状态。
为了将计算器的状态改为初始状态,我们需要清空所有我们设置的自定义属性。
if (action === 'clear') {if (key.textContent === 'AC') {calculator.dataset.firstValue = ''calculator.dataset.modValue = ''calculator.dataset.operator = ''calculator.dataset.previousKeyType = ''} else {key.textContent = 'AC'}
就是这样~反正是边缘用例!
你可以通过这个连接获取源码这个链接(滚动到最下面然后输入你的邮箱地址,我将会发送源码到你的邮箱)。
我们创建的代码是相当混乱的。如果你尝试自己阅读代码可能会比较混乱,让我们一起重构一下它。
重构代码
When you refactor, you’ll often start with the most obvious improvements. In this case, let’s start withcalculate
.
当你重构时,常常会从最明显的地方进行改进。在这种情况下,让我们从calculate
开始。
在重构开始之前,请确保你了解 JavaScript 的这些特性,我们将在重构中使用到。
- 提前返回
- 三目运算符
- 纯函数
- ES6
让我们开始吧!
重构计算方法
这是我们目前知道的。
const calculate = (n1, operator, n2) => {let result = ''if (operator === 'add') {result = firstNum + parseFloat(n2)} else if (operator === 'subtract') {result = parseFloat(n1) - parseFloat(n2)} else if (operator === 'multiply') {result = parseFloat(n1) * parseFloat(n2)} else if (operator === 'divide') {result = parseFloat(n1) / parseFloat(n2)}
你知道的,我们应该尽可能的减少赋值操作。在这里,如果在if
和else if
中返回计算结果的话,我们就可以删除赋值语句:
const calculate = (n1, operator, n2) => {if (operator === 'add') {return firstNum + parseFloat(n2)} else if (operator === 'subtract') {return parseFloat(n1) - parseFloat(n2)} else if (operator === 'multiply') {return parseFloat(n1) * parseFloat(n2)} else if (operator === 'divide') {return parseFloat(n1) / parseFloat(n2)}}
由于所有的情况都需要返回结果,我们可以使用提前返回。如果这样,就不需要任何的else if
条件。
const calculate = (n1, operator, n2) => {if (operator === 'add') {return firstNum + parseFloat(n2)}if (operator === 'subtract') {return parseFloat(n1) - parseFloat(n2)}if (operator === 'multiply') {return parseFloat(n1) * parseFloat(n2)}
由于我们每个if
条件只有一条语句,我们可以去掉括号。(注意:有些开发人员发誓要用大括号)。下面是代码的样子。
const calculate = (n1, operator, n2) => {if (operator === 'add') return parseFloat(n1) + parseFloat(n2)if (operator === 'subtract') return parseFloat(n1) - parseFloat(n2)if (operator === 'multiply') return parseFloat(n1) * parseFloat(n2)if (operator === 'divide') return parseFloat(n1) / parseFloat(n2)}
最后,我们在函数中调用了八次parseFloat
。我们可以通过创建两个变量来包含浮点值来简化它:
const calculate = (n1, operator, n2) => {const firstNum = parseFloat(n1)const secondNum = parseFloat(n2)if (operator === 'add') return firstNum + secondNumif (operator === 'subtract') return firstNum - secondNumif (operator === 'multiply') return firstNum * secondNumif (operator === 'divide') return firstNum / secondNum}
calculate
的重构工作就到此为止了,你不觉得比以前更容易阅读吗?
重构事件监听
代码中用来进行事件监听的部分太冗余了,这是我们目前的情况:
keys.addEventListener('click', e => {if (e.target.matches('button')) {if (!action) { /* ... */ }if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') {/* ... */}if (action === 'clear') { /* ... */ }if (action !== 'clear') { /* ... */ }if (action === 'calculate') { /* ... */ }}})
如何开始重构这段代码呢?如果你不了解任何更好的代码写法。你可能会把每种操作细分来重构这部分代码:
// Don't do this!const handleNumberKeys = (/* ... /) => {/ ... /}const handleOperatorKeys = (/ ... /) => {/ ... /}const handleDecimalKey = (/ ... /) => {/ ... /}const handleClearKey = (/ ... /) => {/ ... /}const handleCalculateKey = (/ ... /) => {/ ... /}
不要做这些,这没有帮助的,因为你仅仅是把代码块分割了,当你做这些,函数将会更难读。
更好的方法是把代码分成纯函数和不纯函数。如果你这样做,你将得到这样的代码:keys.addEventListener('click', e => { // Pure function const resultString = createResultString(/
... */)
// Impure stuff display.textContent = resultString updateCalculatorState(/* ... */) })
这里createResultString
是一个纯函数,我们需要把它的返回值显示在计算器上,updateCalculatorState
是一个不纯函数,可以改变计算器的自定义属性和外观。
实现 createResultString
像之前所说的,createResultString
的返回值需要显示在计算器上,你可以通过display.textContent = ‘some value`.来得到这部分值。
display.textContent = 'some value'
而不是display.textContent = 'some value'
,我们要返回每个值,以便我们以后可以使用它。
// replace the above with thisreturn 'some value'
让我们一起开始,一步一步实现,首先从数字键开始。
实现数字键的结果字符串
这是关于数字键的代码:
if (!action) {if (displayedNum === '0' ||previousKeyType === 'operator' ||previousKeyType === 'calculate') {display.textContent = keyContent} else {display.textContent = displayedNum + keyContent}calculator.dataset.previousKeyType = 'number'}
第一步是将display.textContent = 'some value'
的部分复制到createResultString
中。当你这样做时,确保你把display.textContent =
改为return
。
const createResultString = () => {if (!action) {if (displayedNum === '0' ||previousKeyType === 'operator' ||previousKeyType === 'calculate') {return keyContent} else {return displayedNum + keyContent}}}
接着,我们把if/else
改成三目运算符:
const createResultString = () => {if (action!) {return displayedNum === '0' ||previousKeyType === 'operator' ||previousKeyType === 'calculate'" />if语句的一致性 在createResultString
中,我们使用以下条件来测试被点击的键的类型:
// If key is numberif (!action) { /* ... */ }// If key is decimalif (action === 'decimal') { /* ... */ }// If key is operatorif (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') { /* ... */}// If key is clearif (action === 'clear') { /* ... */ }
它们不一致,所以很难读懂。如果可能的话,我们想让它们保持一致,这样我们就可以这样写:
if (keyType === 'number') { /
... _/ } if (keyType === 'decimal') { /_ ... _/ } if (keyType === 'operator') { /_ ... _/} if (keyType === 'clear') { /_ ... _/ } if (keyType === 'calculate') { /_ ... */ }
为此,我们可以创建一个名为getKeyType
的函数。这个函数应该返回被点击的键的类型。
const getKeyType = (key) => {const { action } = key.datasetif (!action) return 'number'if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') return 'operator'// For everything else, return the actionreturn action}
下面是你如何使用这个函数:
const createResultString = (key, displayedNum, state) => {const keyType = getKeyType(key)
我们完成了createResultString
。让我们继续进行updateCalculatorState
。
实现updateCalculatorState
updateCalculatorState
是一个改变计算器的外观和自定义属性的函数。
与createResultString
一样,我们需要检查被点击的键的类型,这里,我们可以重复使用getKeyType
。在这里,我们可以重复使用getKeyType
。
const updateCalculatorState = (key) => {const keyType = getKeyType(key)
如果你看一下剩下的代码,你可能会注意到我们为每一种类型的键改变了data-previous-key-type
。下面是代码的样子:
const updateCalculatorState = (key, calculator) => {const keyType = getKeyType(key)if (!action) {// ...calculator.dataset.previousKeyType = 'number'}if (action === 'decimal') {// ...calculator.dataset.previousKeyType = 'decimal'}if (action === 'add' ||action === 'subtract' ||action === 'multiply' ||action === 'divide') {// ...calculator.dataset.previousKeyType = 'operator'}if (action === 'clear') {// ...calculator.dataset.previousKeyType = 'clear'}
这是多余的,因为我们已经通过getKeyType
知道按键类型。我们可以将上述内容修改为:
const updateCalculatorState = (key, calculator) => {const keyType = getKeyType(key)calculator.dataset.previousKeyType = keyType
在updateCalculatorState
实现操作键的状态变化
从视图上看,我们需要重设所有按键的点击状态,这里我们可以复制之前的代码:
const updateCalculatorState = (key, calculator) => {const keyType = getKeyType(key)calculator.dataset.previousKeyType = keyType
这是我们为操作键所写的部分中,在把与display.textContent
相关的部分移到createResultString
中后,剩下的内容。
if (keyType === 'operator') {if (firstValue &&operator &&previousKeyType !== 'operator' &&previousKeyType !== 'calculate') {calculator.dataset.firstValue = calculatedValue} else {calculator.dataset.firstValue = displayedNum}
你可能会注意到,我们可以用三元操作符来缩短代码。
if (keyType === 'operator') {key.classList.add('is-depressed')calculator.dataset.operator = key.dataset.actioncalculator.dataset.firstValue = firstValue &&operator &&previousKeyType !== 'operator' &&previousKeyType !== 'calculate'? calculatedValue: displayedNum}
和以前一样,注意你需要的变量和属性。这里,我们需要calculatedValue
和displayedNum
。
const updateCalculatorState = (key, calculator) => {// Variables and properties needed// 1. key// 2. calculator// 3. calculatedValue// 4. displayedNum}
在updateCalculatorState
中实现清除键的的状态变化
这是清除键的剩余代码:
if (action === 'clear') {if (key.textContent === 'AC') {calculator.dataset.firstValue = ''calculator.dataset.modValue = ''calculator.dataset.operator = ''calculator.dataset.previousKeyType = ''} else {key.textContent = 'AC'}}
这里没有什么可以重构的。可以随意复制/粘贴所有内容到updateCalculatorState
中。
在updateCalculatorState
中实现等号键的的状态变化
这是等号键的代码:
if (action === 'calculate') {let firstValue = calculator.dataset.firstValueconst operator = calculator.dataset.operatorlet secondValue = displayedNumif (firstValue) {if (previousKeyType === 'calculate') {firstValue = displayedNumsecondValue = calculator.dataset.modValue}display.textContent = calculate(firstValue, operator, secondValue)}calculator.dataset.modValue = secondValuecalculator.dataset.previousKeyType = 'calculate'}
下面是我们删除所有涉及display.textContent
的内容后剩下的内容。
if (action === 'calculate') {let secondValue = displayedNumif (firstValue) {if (previousKeyType === 'calculate') {secondValue = calculator.dataset.modValue}}
我们可以将其重构为以下内容:
if (keyType === 'calculate') {calculator.dataset.modValue = firstValue && previousKeyType === 'calculate'? modValue: displayedNum}
一如既往,注意使用的属性和变量:
const updateCalculatorState = (key, calculator) => {// Variables and properties needed// 1. key// 2. calculator// 3. calculatedValue// 4. displayedNum// 5. modValue}
传入必要的变量
我们需要给updateCalculatorState
传入五个参数:
key
calculator
calculatedValue
displayedNum
modValue
由于modValue
可以从calculator.dataset
中获取,所以我们只需要传入四个值。
const updateCalculatorState = (key, calculator, calculatedValue, displayedNum) => {// ...}keys.addEventListener('click', e => {if (e.target.matches('button')) returnconst key = e.targetconst displayedNum = display.textContentconst resultString = createResultString(key, displayedNum, calculator.dataset)display.textContent = resultString
再次重构 updateCalculatorState
我们改变了 "updateCalculatorState "中的三种值。
calculator.dataset
- 操作键的按下/未按下的类
AC
和CE
文字
如果您想让它更简洁,您可以将(2)和(3)拆分成另一个函数————updateVisualState
。下面是updateVisualState
。
const updateVisualState = (key, calculator) => {const keyType = getKeyType(key)Array.from(key.parentNode.children).forEach(k => k.classList.remove('is-depressed'))if (keyType === 'operator') key.classList.add('is-depressed')if (keyType === 'clear' && key.textContent !== 'AC') {key.textContent = 'AC'}
收尾工作
重构后的代码变得更加简洁。如果你研究一下事件监听器,你就会知道每个函数的作用。下面是事件监听器最后的样子:
keys.addEventListener('click', e => {if (e.target.matches('button')) returnconst key = e.targetconst displayedNum = display.textContent// Pure functionsconst resultString = createResultString(key, displayedNum, calculator.dataset)
如果对Python有兴趣,想了解更多的Python以及AIoT知识,解决测试问题,以及入门指导,帮你解决学习Python中遇到的困惑,我们这里有技术高手。如果你正在找工作或者刚刚学校出来,又或者已经工作但是经常觉得难点很多,觉得自己Python方面学的不够精想要继续学习的,想转行怕学不会的, 都可以加入我们,可领取最新Python大厂面试资料和Python爬虫、人工智能、学习资料!VX【pydby01】暗号CSDN