javascript计算器_如何使用JavaScript从头开始构建HTML计算器应用

javascript计算器

This is an epic article where you learn how to build a calculator from scratch. We’ll focus on the JavaScript you need to write—how to think about building the calculator, how to write the code, and eventually, how to clean up your code.

这是一篇史诗般的文章,您可以在其中学习如何从头开始构建计算器。 我们将专注于您需要编写JavaScript,即如何考虑如何构建计算器,如何编写代码以及最终如何清理代码。

By the end of the article, you should get a calculator that functions exactly like an iPhone calculator (without the +/- and percentage functionalities).

在文章结尾,您应该获得一个功能与iPhone计算器完全一样的计算器(没有+/-和百分比功能)。

前提条件 (The prerequisites)

Before you attempt follow through the lesson, please make sure you have a decent command of JavaScript. Minimally, you need to know these things:

在尝试完成本课程之前,请确保您具有不错JavaScript命令。 至少,您需要了解以下内容:

  1. If/else statements

    如果/其他语句

  2. For loops

    对于循环

  3. JavaScript functions

    JavaScript函数

  4. Arrow functions

    箭头功能

  5. && and || operators

    &&|| 经营者

  6. How to change the text with the textContent property

    如何使用textContent属性更改文本

  7. How to add event listeners with the event delegation pattern

    如何使用事件委托模式添加事件侦听器

在你开始之前 (Before you begin)

I urge you to try and build the calculator yourself before following the lesson. It’s good practice, because you’ll train yourself to think like a developer.

我强烈建议您在上课之前尝试自己构建计算器。 这是一个好习惯,因为您将训练自己像开发人员一样思考。

Come back to this lesson once you’ve tried for one hour (doesn’t matter whether you succeed or fail. When you try, you think, and that’ll help you absorb the lesson in double quick time).

尝试了一个小时之后,请回到本课程(成功与否无关紧要。尝试时,您会认为,这将帮助您在双倍的时间内吸收课程内容)。

With that, let’s begin by understanding how a calculator works.

这样,让我们​​开始了解计算器的工作原理。

建立计算器 (Building the calculator)

First, we want to build the calculator.

首先,我们要构建计算器。

The calculator consist of two parts: the display and the keys.

计算器由两部分组成:显示屏和按键。

<div class=”calculator”>
  <div class=”calculator__display”>0</div>
  <div class=”calculator__keys”> … </div>
</div>

We can use CSS Grid to make the keys, since they’re arranged in a grid-like format. This has already been done for you in the starter file. You can find the starter file at this pen.

由于它们以类似网格的格式排列,因此我们可以使用CSS Grid来制作键。 起始文件中已为您完成了此操作。 您可以在此笔上找到入门文件。

.calculator__keys { 
  display: grid; 
  /* other necessary CSS */ 
}

To help us identify operator, decimal, clear, and equal keys, we’re going to supply a data-action attribute that describes what they do.

为了帮助我们识别运算符,十进制键,清除键和相等键,我们将提供一个描述其作用的数据操作属性。

<div class="calculator__keys">
  <button class="key--operator" data-action="add">+</button>
  <button class="key--operator" data-action="subtract">-</button
  <button class="key--operator" data-action="multiply">&times;</button>
  <button class="key--operator" data-action="divide">÷</button
  <button>7</button>
  <button>8</button>
  <button>9</button>
  <button>4</button>
  <button>5</button>
  <button>6</button>
  <button>1</button>
  <button>2</button>
  <button>3</button>
  <button>0</button>
  <button data-action="decimal">.</button>
  <button data-action="clear">AC</button>
  <button class="key--equal" data-action="calculate">=</button>
</div>

聆听按键 (Listening to key-presses)

Five things can happen when a person gets hold of a calculator. They can hit:

一个人拿起计算器可能会发生五件事。 他们可以击中:

  1. a number key (0–9)

    数字键(0–9)
  2. an operator key (+, -, ×, ÷)

    操作员键(+,-,×,÷)
  3. the decimal key

    十进制键
  4. the equals key

    等于键
  5. the clear key

    清除键

The first steps to building this calculator are to be able to (1) listen for all keypresses and (2) determine the type of key that is pressed. In this case, we can use an event delegation pattern to listen, since keys are all children of .calculator__keys.

构建此计算器的第一步是能够(1)监听所有按键,并且(2)确定所按下键的类型。 在这种情况下,我们可以使用事件委托模式来监听,因为键都是.calculator__keys.calculator__keys

const calculator = document.querySelector(‘.calculator’)
const keys = calculator.querySelector(‘.calculator__keys’)

keys.addEventListener(‘click’, e => {
 if (e.target.matches(‘button’)) {
   // Do something
 }
})

Next, we can use the data-action attribute to determine the type of key that is clicked.

接下来,我们可以使用data-action属性来确定所单击键的类型。

const key = e.target
const action = key.dataset.action

If the key does not have a data-action attribute, it must be a number key.

如果键没有data-action属性,则它必须是数字键。

if (!action) {
  console.log('number key!')
}

If the key has a data-action that is either add, subtract, multiply or divide, we know the key is an operator.

如果键具有addsubtractmultiplydividedata-action ,我们知道键是一个运算符。

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  console.log('operator key!')
}

If the key’s data-action is decimal, we know the user clicked on the decimal key.

如果键的data-actiondecimal ,则我们知道用户单击了十进制键。

Following the same thought process, if the key’s data-action is clear, we know the user clicked on the clear (the one that says AC) key. If the key’s data-action is calculate, we know the user clicked on the equals key.

按照相同的思考过程,如果密钥的data-actionclear ,则我们知道用户单击了清除(表示AC)的密钥。 如果键的data-actioncalculate ,则我们知道用户单击了equals键。

if (action === 'decimal') {
  console.log('decimal key!')
}

if (action === 'clear') {
  console.log('clear key!')
}

if (action === 'calculate') {
  console.log('equal key!')
}

At this point, you should get a console.log response from every calculator key.

此时,您应该从每个计算器键获得console.log响应。

建立幸福的道路 (Building the happy path)

Let’s consider what the average person would do when they pick up a calculator. This “what the average person would do” is called the happy path.

让我们考虑一下普通人拿起计算器时会做什么。 这种“普通人会做什么”被称为幸福之路

Let’s call our average person Mary.

让我们称我们的普通人玛丽。

When Mary picks up a calculator, she could hit any of these keys:

当玛丽拿起计算器时,她可以按以下任何键:

  1. a number key (0–9)

    数字键(0–9)
  2. an operator key (+, -, ×, ÷)

    操作员键(+,-,×,÷)
  3. the decimal key

    十进制键
  4. the equal key

    等号
  5. the clear key

    清除键

It can be overwhelming to consider five types of keys at once, so let’s take it step by step.

一次考虑五种类型的键可能会让人不知所措,因此让我们逐步进行吧。

当用户按下数字键时 (When a user hits a number key)

At this point, if the calculator shows 0 (the default number), the target number should replace zero.

此时,如果计算器显示0(默认数字),则目标数字应替换为零。

If the calculator shows a non-zero number, the target number should be appended to the displayed number.

如果计算器显示非零数字,则目标数字应附加到显示的数字之后。

Here, we need to know two things:

在这里,我们需要知道两件事:

  1. The number of the key that was clicked

    单击的键号
  2. The current displayed number

    当前显示的数字

We can get these two values through the textContent property of the clicked key and .calculator__display , respectively.

我们可以通过单击键的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
    // ...
  }
})

If the calculator shows 0, we want to replace the calculator’s display with the clicked key. We can do so by replacing the display’s textContent property.

如果计算器显示为0,我们想用单击的键替换计算器的显示。 我们可以通过替换显示器的textContent属性来实现。

if (!action) {
  if (displayedNum === '0') {
    display.textContent = keyContent
  }
}

If the calculator shows a non-zero number, we want to append the clicked key to the displayed number. To append a number, we concatenate a string.

如果计算器显示一个非零数字,我们想将单击的键附加到显示的数字上。 要附加数字,我们连接一个字符串。

if (!action) {
  if (displayedNum === '0') {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
}

At this point, Mary may click either of these keys:

此时,Mary可以单击以下任一键:

  1. A decimal key

    十进制键
  2. An operator key

    操作员钥匙

Let’s say Mary hits the decimal key.

假设玛丽打了十进制键。

当用户按十进制键时 (When a user hits the decimal key)

When Mary hits the decimal key, a decimal should appear on the display. If Mary hits any number after hitting a decimal key, the number should be appended on the display as well.

当Mary按下小数点键时,显示屏上应出现小数点。 如果Mary在按了十进制键后又击中了任何数字,则该数字也应附加在显示屏上。

To create this effect, we can concatenate . to the displayed number.

要创建此效果,我们可以串联. 到显示的数字。

if (action === 'decimal') {
  display.textContent = displayedNum + '.'
}

Next, let’s say Mary continues her calculation by hitting an operator key.

接下来,假设玛丽通过按操作员键继续进行计算。

用户按下操作员键时 (When a user hits an operator key)

If Mary hits an operator key, the operator should be highlighted so Mary knows the operator is active.

如果Mary按下了操作员键,则应突出显示该操作员,以便Mary知道该操作员处于活动状态。

To do so, we can add the is-depressed class to the operator key.

为此,我们可以将is-depressed类添加到操作员键中。

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  key.classList.add('is-depressed')
}

Once Mary has hit an operator key, she’ll hit another number key.

玛丽按下了操作员键后,她将再次按下数字键。

当用户在操作员键之后按数字键时 (When a user hits a number key after an operator key)

When Mary hits a number key again, the previous display should be replaced with the new number. The operator key should also release its pressed state.

当Mary再次按数字键时,以前的显示应替换为新的数字。 操作员键也应释放其按下状态。

To release the pressed state, we remove the is-depressed class from all keys through a forEach loop:

要释放按下状态,我们通过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'))
  }
})

Next, we want to update the display to the clicked key. Before we do this, we need a way to tell if the previous key is an operator key.

接下来,我们要将显示更新为单击的键。 在执行此操作之前,我们需要一种方法来判断上一个键是否为操作员键。

One way to do this is through a custom attribute. Let’s call this custom attribute data-previous-key-type.

一种方法是通过自定义属性。 我们将此自定义属性称为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
  }
}

Next, let’s say Mary decides to complete her calculation by hitting the equals key.

接下来,假设玛丽决定通过按下等号键来完成她的计算。

当用户按下等号键时 (When a user hits the equals key)

When Mary hits the equals key, the calculator should calculate a result that depends on three values:

当Mary按下equals键时,计算器应计算取决于三个值的结果:

  1. The first number entered into the calculator

    输入计算器的第一个数字

  2. The operator

    操作员

  3. The second number entered into the calculator

    输入计算器的第二个数字

After the calculation, the result should replace the displayed value.

计算后,结果应替换显示的值。

At this point, we only know the second number — that is, the currently displayed number.

此时,我们只知道第二个数字 ,即当前显示的数字。

if (action === 'calculate') {
  const secondValue = displayedNum
  // ...
}

To get the first number, we need to store the calculator’s displayed value before we wipe it clean. One way to save this first number is to add it to a custom attribute when the operator button gets clicked.

要获得第一个数字 ,我们需要先存储计算器的显示值,然后再将其擦干净。 保存第一个数字的一​​种方法是在单击操作员按钮时将其添加到自定义属性。

To get the operator, we can also use the same technique.

为了获得运算符 ,我们还可以使用相同的技术。

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  // ...
  calculator.dataset.firstValue = displayedNum
  calculator.dataset.operator = action
}

Once we have the three values we need, we can perform a calculation. Eventually, we want the code to look something like this:

一旦获得所需的三个值,就可以执行计算。 最终,我们希望代码看起来像这样:

if (action === 'calculate') {
  const firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
  display.textContent = calculate(firstValue, operator, secondValue)
}

That means we need to create a calculate function. It should take in three parameters: the first number, the operator, and the second number.

这意味着我们需要创建一个calculate函数。 它应该包含三个参数:第一个数字,运算符和第二个数字。

const calculate = (n1, operator, n2) => {
  // Perform calculation and return calculated value
}

If the operator is add, we want to add values together. If the operator is subtract, we want to subtract the values, and so on.

如果运算符是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
  }
  
  return result
}

Remember that firstValue and secondValue are strings at this point. If you add strings together, you’ll concatenate them (1 + 1 = 11).

请记住, firstValuesecondValue是字符串。 如果将字符串加在一起,则将它们串联起来( 1 + 1 = 11 )。

So, before calculating the result, we want to convert strings to numbers. We can do so with the two functions parseInt and parseFloat.

因此,在计算结果之前,我们希望将字符串转换为数字。 我们可以使用两个函数parseIntparseFloat做到这一点。

  • parseInt converts a string into an integer.

    parseInt将字符串转换为整数

  • parseFloat converts a string into a float (this means a number with decimal places).

    parseFloat将字符串转换为浮点数 (这意味着带小数位的数字)。

For a calculator, we need a float.

对于计算器,我们需要一个浮点数。

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)
  }
  
  return result
}

That’s it for the happy path!

这就是快乐之路!

You can grab the source code for the happy path through this link (scroll down and enter your email address in the box, and I’ll send the source codes right to your mailbox).

您可以通过此链接获取幸福路径的源代码(向下滚动并在框中输入您的电子邮件地址,我会将源代码直接发送到您的邮箱)。

边缘情况 (The edge cases)

The hapy path isn’t enough. To build a calculator that’s robust, you need to make your calculator resilient to weird input patterns. To do so, you have to imagine a troublemaker who tries to break your calculator by hitting keys in the wrong order. Let’s call this troublemaker Tim.

单调的路径还不够。 要构建强大的计算器,您需要使计算器能够适应怪异的输入模式。 为此,您必须想象一个麻烦制造者试图按错误的顺序敲击键盘来破坏计算器。 我们称其为麻烦制造者蒂姆。

Tim can hit these keys in any order:

蒂姆可以按任何顺序按下这些键:

  1. A number key (0–9)

    数字键(0–9)
  2. An operator key (+, -, ×, ÷)

    操作员键(+,-,×,÷)
  3. The decimal key

    十进制键
  4. The equals key

    等于键
  5. The clear key

    清除键

如果Tim按下十进制键会发生什么 (What happens if Tim hits the decimal key)

If Tim hits a decimal key when the display already shows a decimal point, nothing should happen.

如果显示屏已显示小数点时Tim按下小数点键,则什么也不会发生。

Here, we can check that the displayed number contains a . with the includes method.

在这里,我们可以检查显示的数字是否包含. 使用includes方法。

includes checks strings for a given match. If a string is found, it returns true; if not, it returns false.

includes给定匹配项的检查字符串。 如果找到一个字符串,则返回true ; 如果不是,则返回false

Note: includes is case sensitive.

注意includes区分大小写。

// Example of how includes work.
const string = 'The hamburgers taste pretty good!'
const hasExclaimation = string.includes('!')
console.log(hasExclaimation) // true

To check if the string already has a dot, we do this:

要检查字符串是否已经有一个点,我们可以这样做:

// Do nothing if string has a dot
if (!displayedNum.includes('.')) {
  display.textContent = displayedNum + '.'
}

Next, if Tim hits the decimal key after hitting an operator key, the display should show 0..

接下来,如果Tim在按了操作员键后又击中了小数点键,则显示屏应显示0.

Here we need to know if the previous key is an operator. We can tell by checking the the custom attribute, data-previous-key-type, we set in the previous lesson.

在这里,我们需要知道上一个键是否为运算符。 我们可以通过检查上一课中设置的自定义属性data-previous-key-type判断。

data-previous-key-type is not complete yet. To correctly identify if previousKeyType is an operator, we need to update previousKeyType for each clicked key.

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'
}

if (action === 'calculate') {
 // ...
  calculator.dataset.previousKeyType = 'calculate'
}

Once we have the correct previousKeyType, we can use it to check if the previous key is an operator.

一旦有了正确的previousKeyType ,我们就可以使用它来检查上一个键是否为运算符。

if (action === 'decimal') {
  if (!displayedNum.includes('.')) {
    display.textContent = displayedNum + '.'
  } else if (previousKeyType === 'operator') {
    display.textContent = '0.'
  }
  
calculator.dataset.previousKeyType = 'decimal'
}

如果Tim按下操作员键会发生什么 (What happens if Tim hits an operator key)

If Tim hits an operator key first, the operator key should light up. (We’ve already covered for this edge case, but how? See if you can identify what we did).

如果蒂姆先按了操作员键,则操作员键应亮起。 (我们已经介绍了这种极端情况,但是如何?看看您是否可以确定我们所做的事情)。

Second, nothing should happen if Tim hits the same operator key multiple times. (We’ve already covered for this edge case as well).

其次,如果Tim多次敲击同一操作员键,则不会发生任何事情。 (我们已经涵盖了这种边缘情况)。

Note: if you want to provide better UX, you can show the operator getting clicked on repeatedly with some CSS changes. We didn’t do it here, but see if you can program that yourself as an extra coding challenge.

注意:如果您想提供更好的用户体验,则可以通过一些CSS更改来显示操作员被反复点击。 我们在这里没有这样做,但是看看您是否可以自己编写程序,这是额外的编码挑战。

Third, if Tim hits another operator key after hitting the first operator key, the first operator key should be released. Then, the second operator key should be depressed. (We covered for this edge case too — but how?).

第三,如果Tim在按下第一个操作员键后又击中另一个操作员键,则应释放第一个操作员键。 然后,应按下第二个操作员键。 (我们也介绍了这种极端情况-但是如何?)。

Fourth, if Tim hits a number, an operator, a number and another operator, in that order, the display should be updated to a calculated value.

第四,如果Tim按此顺序命中一个数字,一个运算符,一个数字和另一个运算符,则显示应更新为计算值。

This means we need to use the calculate function when firstValue, operator and secondValue exist.

这意味着当firstValueoperatorsecondValue存在时,我们需要使用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)
  }
  
key.classList.add('is-depressed')
  calculator.dataset.previousKeyType = 'operator'
  calculator.dataset.firstValue = displayedNum
  calculator.dataset.operator = action
}

Although we can calculate a value when the operator key is clicked for a second time, we have also introduced a bug at this point — additional clicks on the operator key calculates a value when it shouldn’t.

尽管我们可以在第二次单击操作员键时计算出一个值,但在这一点上,我们还引入了一个错误-在操作员键上不需额外单击就可以计算出一个值。

To prevent the calculator from performing a calculation on subsequent clicks on the operator key, we need to check if the previousKeyType is an operator. If it is, we don’t perform a calculation.

为了防止计算器对操作员键的后续单击执行计算,我们需要检查previousKeyType是否为操作员。 如果是这样,我们将不执行计算。

if (
  firstValue &&
  operator &&
  previousKeyType !== 'operator'
) {
  display.textContent = calculate(firstValue, operator, secondValue)
}

Fifth, after the operator key calculates a number, if Tim hits on a number, followed by another operator, the operator should continue with the calculation, like this: 8 - 1 = 7, 7 - 2 = 5, 5 - 3 = 2.

第五,在操作员键计算出数字后,如果Tim碰到一个数字,然后再由另一位操作员击中,则该操作员应继续进行计算,如下所示: 8 - 1 = 7 7 - 2 = 5 5 - 3 = 2

Right now, our calculator cannot make consecutive calculations. The second calculated value is wrong. Here’s what we have: 99 - 1 = 98, 98 - 1 = 0.

目前,我们的计算器无法进行连续的计算。 第二个计算值是错误的。 这就是我们拥有的: 99 - 1 = 98 98 - 1 = 0

The second value is calculated wrongly, because we fed the wrong values into the calculate function. Let’s go through a few pictures to understand what our code does.

第二个值计算错误,因为我们将错误的值输入了calculate函数。 让我们浏览一些图片以了解我们的代码的作用。

了解我们的计算功能 (Understanding our calculate function)

First, let’s say a user clicks on a number, 99. At this point, nothing is registered in the calculator yet.

首先,假设用户点击了数字99。此时,计算器中尚未注册任何内容。

Second, let’s say the user clicks the subtract operator. After they click the subtract operator, we set firstValue to 99. We set also operator to subtract.

其次,假设用户单击了减法运算符。 在他们单击减法运算符后,我们将firstValue设置为99。我们还将operator设置为减法。

Third, let’s say the user clicks on a second value — this time, it’s 1. At this point, the displayed number gets updated to 1, but our firstValue, operator and secondValue remain unchanged.

第三,假设用户单击了第二个值-这次是1。这时,显示的数字更新为1,但我们的firstValueoperatorsecondValue保持不变。

Fourth, the user clicks on subtract again. Right after they click subtract, before we calculate the result, we set secondValue as the displayed number.

第四,用户再次单击减法。 在他们单击减法之后,在计算结果之前,我们将secondValue设置为显示的数字。

Fifth, we perform the calculation with firstValue 99, operator subtract, and secondValue 1. The result is 98.

第五,我们使用firstValue 99, operator减和secondValue 1进行计算。结果为98。

Once the result is calculated, we set the display to the result. Then, we set operator to subtract, and firstValue to the previous displayed number.

计算结果后,我们将显示设置为结果。 然后,我们将operator设置为减,并将firstValue设置为先前显示的数字。

Well, that’s terribly wrong! If we want to continue with the calculation, we need to update firstValue with the calculated value.

好吧,那是完全错误的! 如果要继续进行计算,则需要使用计算出的值更新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
}

key.classList.add('is-depressed')
calculator.dataset.previousKeyType = 'operator'
calculator.dataset.operator = action

With this fix, consecutive calculations done by operator keys should now be correct.

有了此修复程序,通过操作员键进行的连续计算现在应该是正确的。

如果Tim按下等号键会怎样? (What happens if Tim hits the equals key?)

First, nothing should happen if Tim hits the equals key before any operator keys.

首先,如果Tim在任何操作员键之前先按下equals键,则不会发生任何事情。

We know that operator keys have not been clicked yet if firstValue is not set to a number. We can use this knowledge to prevent the equals from calculating.

我们知道,如果未将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)
  }
  
calculator.dataset.previousKeyType = 'calculate'
}

Second, if Tim hits a number, followed by an operator, followed by a equals, the calculator should calculate the result such that:

其次,如果Tim命中一个数字,然后是一个运算符,然后是一个等于数,则计算器应计算出以下结果:

  1. 2 + = —> 2 + 2 = 4

    2 + = —> 2 + 2 = 4

  2. 2 - = —> 2 - 2 = 0

    2 - = -> 2 - 2 = 0

  3. 2 × = —> 2 × 2 = 4

    2 × = —> 2 × 2 = 4

  4. 2 ÷ = —> 2 ÷ 2 = 1

    2 ÷ = -> 2 ÷ 2 = 1

We have already taken this weird input into account. Can you understand why? :)

我们已经考虑了这个奇怪的输入。 你能明白为什么吗? :)

Third, if Tim hits the equals key after a calculation is completed, another calculation should be performed again. Here’s how the calculation should read:

第三,如果在完成计算后Tim按下了equals键,则应再次执行另一次计算。 计算方法如下:

  1. Tim hits keys 5–1

    蒂姆按5-1
  2. Tim hits equal. Calculated value is 5 - 1 = 4

    蒂姆命中率相等。 计算值是5 - 1 = 4

  3. Tim hits equal. Calculated value is 4 - 1 = 3

    蒂姆命中率相等。 计算值是4 - 1 = 3

  4. Tim hits equal. Calculated value is 3 - 1 = 2

    蒂姆命中率相等。 计算值是3 - 1 = 2

  5. Tim hits equal. Calculated value is 2 - 1 = 1

    蒂姆命中率相等。 计算值是2 - 1 = 1

  6. Tim hits equal. Calculated value is 1 - 1 = 0

    蒂姆命中率相等。 计算值是1 - 1 = 0

Unfortunately, our calculator messes this calculation up. Here’s what our calculator shows:

不幸的是,我们的计算器搞砸了这个计算。 这是我们的计算器显示的内容:

  1. Tim hits key 5–1

    蒂姆击中关键5-1
  2. Tim hits equal. Calculated value is 4

    蒂姆命中率相等。 计算值为4

  3. Tim hits equal. Calculated value is 1

    蒂姆命中率相等。 计算值为1

更正计算 (Correcting the calculation)

First, let’s say our user clicks 5. At this point, nothing is registered in the calculator yet.

首先,假设我们的用户单击了5。这时,计算器中尚未注册任何内容。

Second, let’s say the user clicks the subtract operator. After they click the subtract operator, we set firstValue to 5. We set also operator to subtract.

其次,假设用户单击了减法运算符。 在他们单击减法运算符后,我们将firstValue设置为5。我们还将operator设置为减法。

Third, the user clicks on a second value. Let’s say it’s 1. At this point, the displayed number gets updated to 1, but our firstValue, operator and secondValue remain unchanged.

第三,用户单击第二个值。 假设它是1。这时,显示的数字将更新为1,但我们的firstValueoperatorsecondValue保持不变。

Fourth, the user clicks the equals key. Right after they click equals, but before the calculation, we set secondValue as displayedNum

第四,用户单击等号键。 他们点击后平等的权利,但在计算之前,我们设置secondValuedisplayedNum

Fifth, the calculator calculates the result of 5 - 1 and gives 4. The result gets updated to the display. firstValue and operator get carried forward to the next calculation since we did not update them.

第五,计算器计算的结果, 5 - 1 ,并给出4 。 结果将更新到显示。 由于我们未更新firstValueoperator因此将其结转到下一个计算。

Sixth, when the user hits equals again, we set secondValue to displayedNum before the calculation.

第六,当用户点击再次等于,我们设置secondValuedisplayedNum计算之前。

You can tell what’s wrong here.

您可以在这里说出什么问题。

Instead of secondValue, we want the set firstValue to the displayed number.

而不是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)
  }
  
calculator.dataset.previousKeyType = 'calculate'
}

We also want to carry forward the previous secondValue into the new calculation. For secondValue to persist to the next calculation, we need to store it in another custom attribute. Let’s call this custom attribute modValue (stands for modifier value).

我们还希望将以前的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)
  }
  
// Set modValue attribute
  calculator.dataset.modValue = secondValue
  calculator.dataset.previousKeyType = 'calculate'
}

If the previousKeyType is calculate, we know we can use calculator.dataset.modValue as secondValue. Once we know this, we can perform the calculation.

如果previousKeyTypecalculate ,我们知道我们可以用calculator.dataset.modValue作为secondValue 。 一旦知道这一点,就可以执行计算。

if (firstValue) {
  if (previousKeyType === 'calculate') {
    firstValue = displayedNum
    secondValue = calculator.dataset.modValue
  }
  
display.textContent = calculate(firstValue, operator, secondValue)
}

With that, we have the correct calculation when the equals key is clicked consecutively.

这样,当连续单击equals键时,我们就可以进行正确的计算。

返回等于键 (Back to the equals key)

Fourth, if Tim hits a decimal key or a number key after the calculator key, the display should be replaced with 0. or the new number respectively.

第四,如果Tim在计算器键之后按了十进制键或数字键,则应分别用0.或新数字代替显示。

Here, instead of just checking if the previousKeyType is operator, we also need to check if it’s calculate.

在这里,我们不仅需要检查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.'
  }
  
calculator.dataset.previousKeyType = 'decimal'
}

Fifth, if Tim hits an operator key right after the equals key, the calculator should not calculate.

第五,如果蒂姆击中操作键右侧的等号键后,计算器应该算。

To do this, we check if the previousKeyType is calculate before performing calculations with operator keys.

为此,我们在使用运算符进行计算之前,检查是否calculatepreviousKeyType

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
  }
  
// ...
}

The clear key has two uses:

清除键有两个用途:

  1. All Clear (denoted by AC) clears everything and resets the calculator to its initial state.

    全清除(由AC表示)将清除所有内容并将计算器重置为其初始状态。

  2. Clear entry (denoted by CE) clears the current entry. It keeps previous numbers in memory.

    清除条目(由CE表示)将清除当前条目。 它将先前的数字保留在内存中。

When the calculator is in its default state, AC should be shown.

当计算器处于默认状态时,应显示AC

First, if Tim hits a key (any key except clear), AC should be changed to CE.

首先,如果Tim按下了一个键(清除键以外的任何键),则应将AC更改为CE

We do this by checking if the data-action is clear. If it’s not clear, we look for the clear button and change its textContent.

我们通过检查data-action是否clear 。 如果clear ,我们寻找清除按钮并更改其textContent

if (action !== 'clear') {
  const clearButton = calculator.querySelector('[data-action=clear]')
  clearButton.textContent = 'CE'
}

Second, if Tim hits CE, the display should read 0. At the same time, CE should be reverted to AC so Tim can reset the calculator to its initial state.**

其次,如果Tim击中CE ,则显示应该显示为0。同时, CE应恢复为AC以便Tim可以将计算器重置为其初始状态。**

if (action === 'clear') {
  display.textContent = 0
  key.textContent = 'AC'
  calculator.dataset.previousKeyType = 'clear'
}

Third, if Tim hits AC, reset the calculator to its initial state.

第三,如果Tim碰到AC ,请将计算器重置为其初始状态。

To reset the calculator to its initial state, we need to clear all custom attributes we’ve set.

要将计算器重置为其初始状态,我们需要清除所有已设置的自定义属性。

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 = 0
  calculator.dataset.previousKeyType = 'clear'
}

That’s it — for the edge cases portion, anyway!

就是这样-无论如何,对于边缘盒部分!

You can grab the source code for the edge cases part through this link (scroll down and enter your email address in the box, and I’ll send the source codes right to your mailbox).

您可以通过此链接获取边缘案例部分的源代码(向下滚动并在框中输入您的电子邮件地址,我会将源代码直接发送到您的邮箱)。

At this point, the code we created together is quite confusing. You’ll probably get lost if you try to read the code on your own. Let’s refactor it to make it cleaner.

在这一点上,我们一起创建的代码非常混乱。 如果您尝试自己阅读代码,则可能会迷路。 让我们对其进行重构以使其更整洁。

重构代码 (Refactoring the code)

When you refactor, you’ll often start with the most obvious improvements. In this case, let’s start with calculate.

重构时,通常会从最明显的改进入手。 在这种情况下,让我们开始与calculate

Before continuing on, make sure you know these JavaScript practices/features. We’ll use them in the refactor.

在继续之前,请确保您了解这些JavaScript做法/功能。 我们将在重构中使用它们。

  1. Early returns

    早期回报

  2. Ternary operators

    三元运算符

  3. Pure functions

    纯功能

  4. ES6 Destructuring

    ES6解构

With that, let’s begin!

这样,让我们​​开始吧!

重构计算功能 (Refactoring the calculate function)

Here’s what we have so far.

到目前为止,这是我们所拥有的。

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)
  }
  
  return result
}

You learned that we should reduce reassignments as much as possible. Here, we can remove assignments if we return the result of the calculation within the if and else if statements:

您了解到,我们应尽可能减少重新分配。 在这里,如果我们在ifelse 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)
  }
}

Since we return all values, we can use early returns. If we do so, there’s no need for any else if conditions.

由于我们返回所有值,因此可以使用早期收益 。 如果我们这样做,则没有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 (operator === 'divide') {
    return parseFloat(n1) / parseFloat(n2)
  }
}

And since we have one statement per if condition, we can remove the brackets. (Note: some developers swear by curly brackets, though). Here's what the code would look like:

并且由于每个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)
}

Finally, we called parseFloat eight times in the function. We can simplify it by creating two variables to contain float values:

最后,我们在函数中调用了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
}

We’re done with calculate now. Don't you think it's easier to read compared to before?

现在完成calculate 。 您不觉得比以前更容易阅读吗?

重构事件监听器 (Refactoring the event listener)

The code we created for the event listener is huge. Here’s what we have at the moment:

我们为事件侦听器创建的代码非常庞大。 这是我们目前拥有的:

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') { /* ... */ }
  }
})

How do you begin refactoring this piece of code? If you don’t know any programming best practices, you may be tempted to refactor by splitting up each kind of action into a smaller function:

您如何开始重构这段代码? 如果您不了解任何编程最佳实践,则可能会尝试通过将每种操作分成一个较小的函数来进行重构:

// Don't do this!
const handleNumberKeys = (/* ... */) => {/* ... */}
const handleOperatorKeys = (/* ... */) => {/* ... */}
const handleDecimalKey = (/* ... */) => {/* ... */}
const handleClearKey = (/* ... */) => {/* ... */}
const handleCalculateKey = (/* ... */) => {/* ... */}

Don’t do this. It doesn’t help, because you’re merely splitting up blocks of code. When you do so, the function gets harder to read.

不要这样 这无济于事,因为您只是在拆分代码块。 这样做时,该函数将变得更难阅读。

A better way is to split the code into pure and impure functions. If you do so, you’ll get code that looks like this:

更好的方法是将代码分为纯函数和不纯函数。 如果这样做,您将获得如下代码:

keys.addEventListener('click', e => {
  // Pure function
  const resultString = createResultString(/* ... */)
  
  // Impure stuff
  display.textContent = resultString
  updateCalculatorState(/* ... */)
})

Here, createResultString is a pure function that returns what needs to be displayed on the calculator. updateCalculatorState is an impure function that changes the calculator's visual appearance and custom attributes.

在这里, createResultString是一个纯函数,它返回需要在计算器上显示的内容。 updateCalculatorState是一个不纯函数,会更改计算器的外观和自定义属性。

制作createResultString (Making createResultString)

As mentioned before, createResultString should return the value that needs to be displayed on the calculator.You can get these values through parts of the code that says display.textContent = 'some value.

如前所述, createResultString应该返回需要在计算器上显示的值。您可以通过显示为display.textContent = 'some value部分代码来获取这些值。

display.textContent = 'some value'

Instead of display.textContent = 'some value', we want to return each value so we can use it later.

我们希望返回每个值,而不是display.textContent = 'some value' ,以便以后使用。

// replace the above with this
return 'some value'

Let’s go through this together, step by step, starting with number keys.

让我们从数字键开始,逐步逐步进行操作。

为数字键制作结果字符串 (Making the result string for number keys)

Here’s the code we have for number keys:

这是数字键的代码:

if (!action) {
  if (
    displayedNum === '0' ||
    previousKeyType === 'operator' ||
    previousKeyType === 'calculate'
  ) {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
  calculator.dataset.previousKeyType = 'number'
}

The first step is to copy parts that say display.textContent = 'some value' into createResultString. When you do this, make sure you change display.textContent = into return.

第一步是将显示为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
    }
  }
}

Next, we can convert the if/else statement to a ternary operator:

接下来,我们可以将if/else语句转换为三元运算符:

const createResultString = () => {
  if (action!) {
    return displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
      ? keyContent
      : displayedNum + keyContent
  }
}

When you refactor, remember to note down a list of variables you need. We’ll come back to the list later.

重构时,请记住记下所需的变量列表。 稍后我们将返回列表。

const createResultString = () => {
  // Variables required are:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
  
  if (action!) {
    return displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
      ? keyContent
      : displayedNum + keyContent
  }
}

为十进制键制作结果字符串 (Making the result string for the decimal key)

Here’s the code we have for the decimal key:

这是十进制键的代码:

if (action === 'decimal') {
  if (!displayedNum.includes('.')) {
    display.textContent = displayedNum + '.'
  } else if (
    previousKeyType === 'operator' ||
    previousKeyType === 'calculate'
  ) {
    display.textContent = '0.'
  }
  
  calculator.dataset.previousKeyType = 'decimal'
}

As before, we want to move anything that changes display.textContentinto createResultString.

和以前一样,我们想将所有将display.textContent更改的内容移到createResultString

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) {
      return = displayedNum + '.'
    } else if (previousKeyType === 'operator' || previousKeyType === 'calculate') {
      return = '0.'
    }
  }
}

Since we want to return all values, we can convert else if statements into early returns.

由于我们要返回所有值,因此可以将else if语句转换为早期返回。

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) return displayedNum + '.'
    if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
  }
}

A common mistake here is to forget to return the currently displayed number when neither condition is matched. We need this because we will replace the display.textContent with the value returned from createResultString. If we missed it, createResultString will return undefined, which is not what we desire.

这是一个常见的错误,就是在两个条件都不匹配时,忘记返回当前显示的数字。 我们需display.textContent ,因为我们将display.textContent替换为createResultString返回的值。 如果我们错过了它, createResultString将返回undefined ,这不是我们想要的。

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) return displayedNum + '.'
    if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
    return displayedNum
  }
}

As always, take note of the variables that are required. At this point, the required variables remain the same as before:

与往常一样,请注意所需的变量。 此时,所需的变量与之前相同:

const createResultString = () => {
  // Variables required are:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
}

为操作员键生成结果字符串 (Making the result string for operator keys)

Here’s the code we wrote for operator keys.

这是我们为操作员键编写的代码。

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
  }
  
  key.classList.add('is-depressed')
  calculator.dataset.previousKeyType = 'operator'
  calculator.dataset.operator = action
}

You know the drill by now: we want to move everything that changes display.textContent into createResultString. Here's what needs to be moved:

您现在就知道了钻取:我们想将所有将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)
    }
  }
}

Remember, createResultString needs to return the value to be displayed on the calculator. If the if condition did not match, we still want to return the displayed number.

请记住, 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
    }
  }
}

We can then refactor the if/else statement into a ternary operator:

然后,我们可以将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
  }
}

If you look closely, you’ll realize that there’s no need to store a secondValuevariable. We can use displayedNum directly in the calculate function.

如果仔细观察,您会发现不需要存储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
  }
}

Finally, take note of the variables and properties required. This time, we need calculator.dataset.firstValue and calculator.dataset.operator.

最后,请注意所需的变量和属性。 这次,我们需要calculator.dataset.firstValuecalculator.dataset.operator

const createResultString = () => {
  // Variables & properties required are:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
  // 5. calculator.dataset.firstValue
  // 6. calculator.dataset.operator
}

为清除键制作结果字符串 (Making the result string for the clear key)

We wrote the following code to handle the clear key.

我们编写了以下代码来处理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 = 0
  calculator.dataset.previousKeyType = 'clear'
}

As above, want to move everything that changes display.textContentinto createResultString.

如上所述,要将更改display.textContent所有内容移到createResultString

const createResultString = () => {
  // ...
  if (action === 'clear') return 0
}

使结果字符串等于等号 (Making the result string for the equals key)

Here’s the code we wrote for the equals key:

这是我们为equals键编写的代码:

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.textContentinto 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
  
  if (firstValue) {
    if (previousKeyType === 'calculate') {
      firstValue = displayedNum
      secondValue = calculator.dataset.modValue
    }
    display.textContent = calculate(firstValue, operator, secondValue)
  }
}

When copying the code into createResultString, make sure you return values for every possible scenario:

将代码复制到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
    }
  }
}

Next, we want to reduce reassignments. We can do so by passing in the correct values into calculate through a ternary operator.

接下来,我们要减少重新分配。 我们可以通过将正确的值传递给三元运算符calculate进行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
    }
  }
}

You can further simplify the above code with another ternary operator if you feel comfortable with it:

如果您愿意,可以使用另一个三元运算符进一步简化上述代码:

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
  }
}

At this point, we want to take note of the properties and variables required again:

在这一点上,我们要再次注意所需的属性和变量:

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
}

传递必要的变量 (Passing in necessary variables)

We need seven properties/variables in createResultString:

我们在createResultString需要七个属性/变量:

  1. keyContent

    keyContent

  2. displayedNum

    displayedNum

  3. previousKeyType

    previousKeyType

  4. action

    action

  5. firstValue

    firstValue

  6. modValue

    modValue

  7. operator

    operator

We can get keyContent and action from key. We can also get firstValue, modValue, operator and previousKeyType from calculator.dataset.

我们可以从key获取keyContentaction 。 我们还可以得到firstValuemodValueoperatorpreviousKeyTypecalculator.dataset

That means the createResultString function needs three variables—key, displayedNum and calculator.dataset. Since calculator.datasetrepresents the state of the calculator, let's use a variable called stateinstead.

这意味着createResultString功能需要三个变量- keydisplayedNumcalculator.dataset 。 由于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)
  
  // ...
})

Feel free to destructure variables if you desire:

如果需要,可以随意分解变量:

const createResultString = (key, displayedNum, state) => {
  const keyContent = key.textContent
  const { action } = key.dataset
  const {
    firstValue,
    modValue,
    operator,
    previousKeyType
  } = state
  
  // ...
}

if语句内的一致性 (Consistency within if statements)

In createResultString, we used the following conditions to test for the type of keys that were clicked:

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 key is calculate
if (action === 'calculate') { /* ... */ }

They’re not consistent, so they’re hard to read. If possible, we want to make them consistent so we can write something like this:

它们不一致,因此很难阅读。 如果可能的话,我们希望使它们一致,所以我们可以这样写:

if (keyType === 'number') { /* ... */ }
if (keyType === 'decimal') { /* ... */ }
if (keyType === 'operator') { /* ... */}
if (keyType === 'clear') { /* ... */ }
if (keyType === 'calculate') { /* ... */ }

To do so, we can create a function called getKeyType. This function should return the type of key that was clicked.

为此,我们可以创建一个名为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
}

Here’s how you’d use the function:

使用此功能的方法如下:

const createResultString = (key, displayedNum, state) => {
  const keyType = getKeyType(key)
  
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

We’re done with createResultString. Let's move on to updateCalculatorState.

我们完成了createResultString 。 让我们继续进行updateCalculatorState

使updateCalculatorState (Making updateCalculatorState)

updateCalculatorState is a function that changes the calculator's visual appearance and custom attributes.

updateCalculatorState是一个函数,可以更改计算器的外观和自定义属性。

As with createResultString, we need to check the type of key that was clicked. Here, we can reuse getKeyType.

createResultString ,我们需要检查单击的键的类型。 在这里,我们可以重用getKeyType

const updateCalculatorState = (key) => {
  const keyType = getKeyType(key)
  
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

If you look at the leftover code, you may notice we change data-previous-key-type for every type of key. Here's what the code looks like:

如果查看剩余的代码,您可能会注意到我们为每种类型的键更改了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'
  }
  
  if (action === 'calculate') {
    calculator.dataset.previousKeyType = 'calculate'
  }
}

This is redundant because we already know the key type with getKeyType. We can refactor the above to:

这是多余的,因为我们已经知道getKeyType的键类型。 我们可以将以上内容重构为:

const updateCalculatorState = (key, calculator) => {
  const keyType = getKeyType(key)
  calculator.dataset.previousKeyType = keyType
    
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

为操作员键制作updateCalculatorState (Making updateCalculatorState for operator keys)

Visually, we need to make sure all keys release their depressed state. Here, we can copy and paste the code we had before:

在视觉上,我们需要确保所有键都释放其按下状态。 在这里,我们可以复制并粘贴以前的代码:

const updateCalculatorState = (key, calculator) => {
  const keyType = getKeyType(key)
  calculator.dataset.previousKeyType = keyType
  
  Array.from(key.parentNode.children).forEach(k => k.classList.remove('is-depressed'))
}

Here’s what’s left from what we’ve written for operator keys, after moving pieces related to display.textContent into createResultString.

在将与display.textContent相关的部分移动到createResultString之后,这就是我们为操作员键编写的内容。

if (keyType === 'operator') {
  if (firstValue &&
      operator &&
      previousKeyType !== 'operator' &&
      previousKeyType !== 'calculate'
  ) {
    calculator.dataset.firstValue = calculatedValue
  } else {
    calculator.dataset.firstValue = displayedNum
  }
  
  key.classList.add('is-depressed')
  calculator.dataset.operator = key.dataset.action
}

You may notice that we can shorten the code with a ternary operator:

您可能会注意到,我们可以使用三元运算符来缩短代码:

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
}

As before, take note of the variables and properties you need. Here, we need calculatedValue and displayedNum.

和以前一样,请注意所需的变量和属性。 在这里,我们需要calculatedValuedisplayedNum

const updateCalculatorState = (key, calculator) => {
  // Variables and properties needed
  // 1. key
  // 2. calculator
  // 3. calculatedValue
  // 4. displayedNum
}

使updateCalculatorState为清除键 (Making updateCalculatorState for the clear key)

Here’s the leftover code for the clear key:

这是清除键的剩余代码:

if (action === 'clear') {
  if (key.textContent === 'AC') {
    calculator.dataset.firstValue = ''
    calculator.dataset.modValue = ''
    calculator.dataset.operator = ''
    calculator.dataset.previousKeyType = ''
  } else {
    key.textContent = 'AC'
  }
}

if (action !== 'clear') {
  const clearButton = calculator.querySelector('[data-action=clear]')
  clearButton.textContent = 'CE'
}

There’s nothing much we can refactor here. Feel free to copy/paste everything into updateCalculatorState.

我们在这里没有什么可以重构的。 随意将所有内容复制/粘贴到updateCalculatorState

为equals键制作updateCalculatorState (Making updateCalculatorState for the equals key)

Here’s the code we wrote for the equals key:

这是我们为equals键编写的代码:

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'
}

Here’s what we’re left with if we remove everything that concerns display.textContent.

如果删除与display.textContent所有内容,那么剩下的就是这些。

if (action === 'calculate') {
  let secondValue = displayedNum
  
  if (firstValue) {
    if (previousKeyType === 'calculate') {
      secondValue = calculator.dataset.modValue
    }
  }
  
  calculator.dataset.modValue = secondValue
}

We can refactor this into the following:

我们可以将其重构为以下内容:

if (keyType === 'calculate') {
  calculator.dataset.modValue = firstValue && previousKeyType === 'calculate'
    ? modValue
    : displayedNum
}

As always, take note of the properties and variables used:

与往常一样,请注意所使用的属性和变量:

const updateCalculatorState = (key, calculator) => {
  // Variables and properties needed
  // 1. key
  // 2. calculator
  // 3. calculatedValue
  // 4. displayedNum
  // 5. modValue
}

传递必要的变量 (Passing in necessary variables)

We know we need five variables/properties for updateCalculatorState:

我们知道我们需要五个变量/属性来进行updateCalculatorState

  1. key

    key

  2. calculator

    calculator

  3. calculatedValue

    calculatedValue

  4. displayedNum

    displayedNum

  5. modValue

    modValue

Since modValue can be retrieved from calculator.dataset, we only need to pass in four values:

由于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
  
  // Pass in necessary values
  updateCalculatorState(key, calculator, resultString, displayedNum)
})

再次重构updateCalculatorState (Refactoring updateCalculatorState again)

We changed three kinds of values in updateCalculatorState:

我们在updateCalculatorState更改了三种值:

  1. calculator.dataset

    calculator.dataset

  2. The class for pressing/depressing operators

    压/压操作员类别
  3. AC vs CE text

    ACCE文字

If you want to make it cleaner, you can split (2) and (3) into another function — updateVisualState. Here's what updateVisualState can look like:

如果要使其更整洁,可以将(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'
  }
  
  if (keyType !== 'clear') {
    const clearButton = calculator.querySelector('[data-action=clear]')
    clearButton.textContent = 'CE'
  }
}

结语 (Wrapping up)

The code become much cleaner after the refactor. If you look into the event listener, you’ll know what each function does. Here’s what the event listener looks like at the end:

重构后,代码变得更加清晰。 如果查看事件侦听器,您将知道每个函数的作用。 下面是事件侦听器的外观:

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)
  
  // Update states
  display.textContent = resultString
  updateCalculatorState(key, calculator, resultString, displayedNum)
  updateVisualState(key, calculator)
})

You can grab the source code for the refactor part through this link (scroll down and enter your email address in the box, and I’ll send the source codes right to your mailbox).

您可以通过此链接获取重构部分的源代码(向下滚动并在框中输入您的电子邮件地址,然后我会将源代码直接发送到您的邮箱)。

I hope you enjoyed this article. If you did, you might love Learn JavaScript—a course where I show you how to build 20 components, step by step, like how we built this calculator today.

希望您喜欢这篇文章。 如果您这样做了,您可能会喜欢Learn JavaScript(这是一门课程,我将逐步向您展示如何构建20个组件,就像我们今天如何构建此计算器一样)。

Note: we can improve the calculator further by adding keyboard support and accessibility features like Live regions. Want to find out how? Go check out Learn JavaScript :)

注意:我们可以通过添加键盘支持和实时区域等辅助功能来进一步改进计算器。 想了解如何? 去看看学习JavaScript :)

翻译自: https://www.freecodecamp.org/news/how-to-build-an-html-calculator-app-from-scratch-using-javascript-4454b8714b98/

javascript计算器

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值