这是一篇很棒的文章,在这里你可以学习如何从零做出一款计算器。我们希望你使用 JavaScript 开发并且思考怎么构建一款计算器, 如何编写代码,以及最后,如何整理自己的代码。
在这篇文章结束,你会得到一款和 iPhone 计算器功能一样的计算器(除了 +/- 和百分比功能外)。
在你开始本节课程前,请确保你对 JavaScript 有一个不错的了解。最起码,你需要知道以下事情:
&&
和 ||
操作符textContent
属性修改文本我建议你在开始课程之前自己尝试下自己开发计算器。这是一个很好的锻炼,因为你会训练自己像开发人员一样思考。
一旦你尝试了一小时,再回来上这节课(不管你是成功还是失败。当年尝试过,思考过,这会帮助你在更短的时间内吸收本节课的内容)。
就这样,我们先来了解下计算器的工作原理。如果对Python有兴趣,想了解更多的Python以及AIoT知识,解决测试问题,以及入门指导,帮你解决学习Python中遇到的困惑,我们这里有技术高手。如果你正在找工作或者刚刚学校出来,又或者已经工作但是经常觉得难点很多,觉得自己Python方面学的不够精想要继续学习的,想转行怕学不会的, 都可以加入我们,可领取最新Python大厂面试资料和Python爬虫、人工智能、学习资料!VX【pydby01】暗号CSDN
首先,我们想要建立计算器。
这个计算机包含两个部分:显示屏和键盘。
0
…
我们使用 CSS Grid 去制作键盘部分,因为他们是类似网格的格式进行排列的。这里已经在启动文件中完成了,你可以在以下地址找到启动文件 此处.
.calculator__keys {
display: grid;
/* other necessary CSS */
}
为了帮助我们区分操作符,小数点,清除符号以及等号,我们将设置一个data-action
属性用来描述他们的功能。
×
7
当一个人拿着几个计算器,他会做五种事情,他们可以点击:
构建这个计算器的第一步是能够监听所有(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.target
const 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。
这个普通人我们就称作 Mary 吧。
当 Mary拿起计算器时,她可能会点击任何一个按键:
一下子要思考五种按键可以能不太容易,所以让我们一步一步来。
如果计算器显示 0(默认数字),此时,目标数字需要替换这个 0。
如果计算器显示的是非零数字,那么目标数字就需要在显示的数字后面添加上。
现在,我们需要知道两件事情:
我们可以通过textContent
和点击按键的.calculator__display
分别获取到这两个值。
const display = document.querySelector('.calculator__display')
keys.addEventListener('click', e => {
if (e.target.matches('button')) {
const key = e.target
const action = key.dataset.action
const keyContent = key.textContent
const 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 keys
Array.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 attribute
calculator.dataset.previousKeyType = 'operator'
}
}
})
If the previousKeyType
is an operator, we want to replace the displayed number with clicked number.
如果previousKeyType
是一个操作符,我们希望可以用当前点击的数字替换当前显示的数字。
const previousKeyType = calculator.dataset.previousKeyType
if (!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 = displayedNum
calculator.dataset.operator = action
}
一旦我们得到了三个我们需要的值,接下来我们就可以进行计算。最终,我们需要实现这样的代码:
if (action === 'calculate') {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const 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 an integer.parseFloat
converts a string into a float (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 点击小数点键之前已经有小数点显示在屏幕上了,那么他点击之后将什么都不会发生。
我们可以利用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 dot
if (!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 首先点击了操作键,那么按键就会高亮。(We’ve already covered for this edge case, but how? See if you can identify what we did).
第二种情况,如果 Tim 多次点击同样的操作键,应该什么都不会发生。(我们也已经涵盖了这种边缘情况)。
注: 如果想要提供更好的用户体验,你可以通过 CSS 来让操作者的反复点击得到反馈。 我们不在这里实现,你可以将这个功能当作一次挑战,看看如何实现。
情况,如果 Tim 在点击一个操作键之后又点击了另外一个操作键,那么第一个按的操作键会被解除点击状态,第二次按的操作键应该被设置成按压状态。(我们也覆盖了这种情况,但如何实现的?)
第四种情况,如果 Tim 点击了一个数字键,一个操作键和另外一个操作键,这种情况下,应当直接显示计算之后的结果。
这就意味着在firstValue
,operator
和secondValue
三个参数存在时,我们需要调用calculate
方法。
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
// Note: It's sufficient to check for firstValue and operator because secondValue always exists
if (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
函数。让我们通过几张图片来了解我们的代码是怎么做的。
首先,我们告诉使用者输入一个数字 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.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (
firstValue &&
operator &&
previousKeyType !== 'operator'
) {
const calcValue = calculate(firstValue, operator, secondValue)
display.textContent = calcValue
// Update calculated value as firstValue
calculator.dataset.firstValue = calcValue
} else {
// If there are no calculations, set displayedNum as the firstValue
calculator.dataset.firstValue = displayedNum
}
修改之后,现在通过操作键进行的连续计算应该是正确的。
第一种情况,Tim 在点击等号前没点击过任何操作键,那么什么都不会发生。
我们知道,如果firstValue
没有设置为数字,也就代表操作键还没有被点击。我们可以利用这个点来防止等号进行计算。
if (action === 'calculate') {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (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 在一次计算完成之后点击了等号键,应该进行另外一次计算,例如这样:
5 - 1
5 - 1 = 4
4 - 1 = 3
3 - 1 = 2
2 - 1 = 1
1 - 1 = 0
不幸的是,我们的把这个计算弄乱了,下面是我们计算的结果:
5 - 1
4
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.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
}
display.textContent = calculate(firstValue, operator, secondValue)
}
我们可能也想把上一次计算的secondValue
带到下一次计算当中。为了做到这个功能,我们需要利用另外的自定义属性来存储它。让我们来定义一个叫modValue
的属性。
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
}
display.textContent = calculate(firstValue, operator, secondValue)
}
如果previousKeyType
是calculate
,我可以使用 calculator.dataset.modValue
作为secondValue
。知道这个的话,我们就可以进行计算
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = 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 = calcValue
calculator.dataset.firstValue = calcValue
} else {
calculator.dataset.firstValue = displayedNum
}
清除键有两种用法:
当计算器处于默认状态时,应该显示 "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 = 0
key.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 with calculate
.
当你重构时,常常会从最明显的地方进行改进。在这种情况下,让我们从calculate
开始。
在重构开始之前,请确保你了解 JavaScript 的这些特性,我们将在重构中使用到。
让我们开始吧!
这是我们目前知道的。
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 + secondNum
if (operator === 'subtract') return firstNum - secondNum
if (operator === 'multiply') return firstNum * secondNum
if (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
的返回值需要显示在计算器上,你可以通过display.textContent = 'some value`.来得到这部分值。
display.textContent = 'some value'
而不是display.textContent = 'some value'
,我们要返回每个值,以便我们以后可以使用它。
// replace the above with this
return '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'
? keyContent
: displayedNum + keyContent
}
}
当你重构时,记得记下你需要变量的清单。我们稍后再来看看这个清单。
const createResultString = () => {
// Variables required are:
// 1. keyContent
// 2. displayedNum
// 3. previousKeyType
// 4. action
这是我们代码中关于小数点键的部分:
if (action === 'decimal') {
if (!displayedNum.includes('.')) {
display.textContent = displayedNum + '.'
} else if (
previousKeyType === 'operator' ||
previousKeyType === 'calculate'
) {
display.textContent = '0.'
}
和之前一样,我们要把任何改变display.textContent
的东西移到createResultString
中。
const createResultString = () => {
// ...
由于我们想要返回所有的值,我们可以将else if
语句转换为提前返回。
const createResultString = () => {
// ...
这里一个常见的错误是当两个条件都不匹配时,忘记返回当前显示的数字。我们需要用createResultString
返回的值替换display.textContent
。如果我们忘记返回值,createResultString
将返回undefined
,这不是我们想要的。
const createResultString = () => {
// ...
和之前一样,记下所需的变量。此时,所需的变量仍与之前相同:
const createResultString = () => {
// Variables required are:
// 1. keyContent
// 2. displayedNum
// 3. previousKeyType
// 4. action
}
这是我们关于操作键的代码。
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (
firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
) {
const calcValue = calculate(firstValue, operator, secondValue)
display.textContent = calcValue
calculator.dataset.firstValue = calcValue
} else {
calculator.dataset.firstValue = displayedNum
}
你现在知道该怎么做了:我们要把改变display.textContent
的所有内容移到createResultString
中。下面是需要移动的内容。
const createResultString = () => {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (
firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
) {
return calculate(firstValue, operator, secondValue)
}
}
}
请记住,createResultString
需要返回要在计算器上显示的值。如果if
条件不匹配,我们仍然要返回显示的数字。
const createResultString = () => {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (
firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
) {
return calculate(firstValue, operator, secondValue)
} else {
return displayedNum
}
}
}
然后我们可以将 if/else
语句重构为三元操作符。
const createResultString = () => {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
return firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
? calculate(firstValue, operator, secondValue)
: displayedNum
}
}
如果你仔细观察,你会发现没有必要存储一个secondValue
变量。我们可以在calculate
函数中直接使用displayedNum
。
const createResultString = () => {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
return firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
? calculate(firstValue, operator, displayedNum)
: displayedNum
}
}
最后,注意一下所需的变量和属性。这次,我们需要calculator.dataset.firstValue
和calculator.dataset.operator
。
const createResultString = () => {
// Variables & properties required are:
// 1. keyContent
// 2. displayedNum
// 3. previousKeyType
// 4. action
// 5. calculator.dataset.firstValue
// 6. calculator.dataset.operator
}
这是我们处理clear
键的代码。
if (action === 'clear') {
if (key.textContent === 'AC') {
calculator.dataset.firstValue = ''
calculator.dataset.modValue = ''
calculator.dataset.operator = ''
calculator.dataset.previousKeyType = ''
} else {
key.textContent = 'AC'
}
如上,我们需要把改变display.textContent
的内容都放到createResultString
中。
const createResultString = () => {
// ...
if (action === 'clear') return 0
}
处理点击等号事件的代码:
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
let secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = calculator.dataset.modValue
}
display.textContent = calculate(firstValue, operator, secondValue)
}
calculator.dataset.modValue = secondValue
calculator.dataset.previousKeyType = 'calculate'
}
As above, we want to copy everything that changes display.textContent
into createResultString
. Here's what needs to be copied:
同样的 我们需要把改变display.textContent
的内容放到 createResultString
中,以下是我们需要复制的:
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
let secondValue = displayedNum
当把代码复制到createResultString
中时,要确保为每一种可能的情况返回值。
const createResultString = () => {
// ...
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
let secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = calculator.dataset.modValue
}
return calculate(firstValue, operator, secondValue)
} else {
return displayedNum
}
}
}
接下来,我们要减少重赋值。我们可以通过三元运算符将正确的值传入calculate
来实现。
const createResultString = () => {
// ...
if (action === 'calculate') {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const modValue = calculator.dataset.modValue
if (firstValue) {
return previousKeyType === 'calculate'
? calculate(displayedNum, operator, modValue)
: calculate(firstValue, operator, displayedNum)
} else {
return displayedNum
}
}
}
如果你觉得舒服的话,可以用另一个三元运算符进一步简化上述代码。
const createResultString = () => {
// ...
if (action === 'calculate') {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const modValue = calculator.dataset.modValue
return firstValue
? previousKeyType === 'calculate'
? calculate(displayedNum, operator, modValue)
: calculate(firstValue, operator, displayedNum)
: displayedNum
}
}
此时,我们要再注意一下所需的属性和变量。
const createResultString = () => {
// Variables & properties required are:
// 1. keyContent
// 2. displayedNum
// 3. previousKeyType
// 4. action
// 5. calculator.dataset.firstValue
// 6. calculator.dataset.operator
// 7. calculator.dataset.modValue
}
We need seven properties/variables in createResultString
:
我们需要向createResultString
传递这些变量/属性:
keyContent
displayedNum
previousKeyType
action
firstValue
modValue
operator
我们可以从key
中得到keyContent
和action
。我们还可以从calculator.dataset
中得到firstValue
、modValue
、operator
和previousKeyType
。
这意味着createResultString
函数需要三个变量key
、displayedNum
和calculator.dataset
。由于calculator.dataset
代表了计算器的状态,所以我们使用一个叫做state
的变量来代替。
const createResultString = (key, displayedNum, state) => {
const keyContent = key.textContent
const action = key.dataset.action
const firstValue = state.firstValue
const modValue = state.modValue
const operator = state.operator
const previousKeyType = state.previousKeyType
// ... Refactor as necessary
}
// Using createResultString
keys.addEventListener('click', e => {
if (e.target.matches('button')) return
const displayedNum = display.textContent
const resultString = createResultString(e.target, displayedNum, calculator.dataset)
如果你愿意的话,可以随意拆分变量。
const createResultString = (key, displayedNum, state) => {
const keyContent = key.textContent
const { action } = key.dataset
const {
firstValue,
modValue,
operator,
previousKeyType
} = state
在createResultString
中,我们使用以下条件来测试被点击的键的类型:
// If key is number
if (!action) { /* ... */ }
// If key is decimal
if (action === 'decimal') { /* ... */ }
// If key is operator
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) { /* ... */}
// If key is clear
if (action === 'clear') { /* ... */ }
它们不一致,所以很难读懂。如果可能的话,我们想让它们保持一致,这样我们就可以这样写:
if (keyType === 'number') { /
... _/ } if (keyType === 'decimal') { /_ ... _/ } if (keyType === 'operator') { /_ ... _/} if (keyType === 'clear') { /_ ... _/ } if (keyType === 'calculate') { /_ ... */ }
为此,我们可以创建一个名为getKeyType
的函数。这个函数应该返回被点击的键的类型。
const getKeyType = (key) => {
const { action } = key.dataset
if (!action) return 'number'
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) return 'operator'
// For everything else, return the action
return action
}
下面是你如何使用这个函数:
const createResultString = (key, displayedNum, state) => {
const keyType = getKeyType(key)
我们完成了createResultString
。让我们继续进行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.action
calculator.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.firstValue
const operator = calculator.dataset.operator
let secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = calculator.dataset.modValue
}
display.textContent = calculate(firstValue, operator, secondValue)
}
calculator.dataset.modValue = secondValue
calculator.dataset.previousKeyType = 'calculate'
}
下面是我们删除所有涉及display.textContent
的内容后剩下的内容。
if (action === 'calculate') {
let secondValue = displayedNum
if (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')) return
const key = e.target
const displayedNum = display.textContent
const resultString = createResultString(key, displayedNum, calculator.dataset)
display.textContent = resultString
我们改变了 "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')) return
const key = e.target
const displayedNum = display.textContent
// Pure functions
const resultString = createResultString(key, displayedNum, calculator.dataset)
如果对Python有兴趣,想了解更多的Python以及AIoT知识,解决测试问题,以及入门指导,帮你解决学习Python中遇到的困惑,我们这里有技术高手。如果你正在找工作或者刚刚学校出来,又或者已经工作但是经常觉得难点很多,觉得自己Python方面学的不够精想要继续学习的,想转行怕学不会的, 都可以加入我们,可领取最新Python大厂面试资料和Python爬虫、人工智能、学习资料!VX【pydby01】暗号CSDN