Это эпическая статья, в которой вы узнаете, как построить калькулятор с нуля. Мы сосредоточимся на JavaScript, который вам нужно написать: как подумать о создании калькулятора, как написать код и, в конечном итоге, как очистить ваш код.

К концу статьи вы должны получить калькулятор, который работает точно так же, как калькулятор iPhone (без функций +/- и процентов).

Предпосылки

Прежде чем пытаться продолжить урок, убедитесь, что вы хорошо владеете JavaScript. Как минимум, вам нужно знать следующее:

  1. Если / еще утверждения
  2. Для петель
  3. Функции JavaScript
  4. Стрелочные функции
  5. && и || операторы
  6. Как изменить текст с помощью свойства textContent
  7. Как добавить слушателей событий с шаблоном делегирования событий

Прежде чем вы начнете

Я настоятельно рекомендую вам попробовать собрать калькулятор самостоятельно, прежде чем продолжить урок. Это хорошая практика, потому что вы научитесь думать как разработчик.

Вернитесь к этому уроку после того, как вы попробовали в течение одного часа (неважно, удастся ли вы или нет. Когда вы пытаетесь, вы думаете, и это поможет вам усвоить урок в два раза быстрее).

Итак, давайте начнем с понимания того, как работает калькулятор.

Создание калькулятора

Во-первых, мы хотим построить калькулятор.

Калькулятор состоит из двух частей: дисплея и клавиш.

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

Мы можем использовать CSS Grid для создания ключей, поскольку они расположены в виде сетки. Это уже было сделано за вас в стартовом файле. Вы можете найти стартовый файл на этой ручке.

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

Чтобы помочь нам идентифицировать операторы, десятичные, четкие и равные ключи, мы собираемся предоставить атрибут data-action, который описывает, что они делают.

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

Прослушивание нажатия клавиш

Когда человек достает калькулятор, может произойти пять вещей. Они могут поразить:

  1. цифровая клавиша (0–9)
  2. клавиша оператора (+, -, ×, ÷)
  3. десятичный ключ
  4. ключ равенства
  5. чистый ключ

Первые шаги к созданию этого калькулятора - это возможность (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 ответ от каждой клавиши калькулятора.

Строим счастливый путь

Давайте посмотрим, что сделает средний человек, взяв в руки калькулятор. Это то, «что сделал бы средний человек», называется счастливым путем.

Назовем обычного человека Мэри.

Когда Мэри берет калькулятор, она может нажать любую из этих клавиш:

  1. цифровая клавиша (0–9)
  2. клавиша оператора (+, -, ×, ÷)
  3. десятичный ключ
  4. равный ключ
  5. чистый ключ

Одновременное рассмотрение пяти типов ключей может оказаться непосильной задачей, поэтому давайте рассмотрим это шаг за шагом.

Когда пользователь нажимает цифровую клавишу

На этом этапе, если калькулятор показывает 0 (число по умолчанию), целевое число должно заменить ноль.

Если калькулятор показывает ненулевое число, целевое число должно быть добавлено к отображаемому числу.

Здесь нам нужно знать две вещи:

  1. Номер нажатой клавиши
  2. Текущее отображаемое число

Мы можем получить эти два значения через свойство 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
  }
}

На этом этапе Мэри может нажать любую из этих клавиш:

  1. Десятичный ключ
  2. Ключ оператора

Допустим, Мэри нажимает десятичную клавишу.

Когда пользователь нажимает десятичную клавишу

Когда Мэри нажимает десятичную клавишу, на дисплее должно появиться десятичное число. Если Мэри наберет любое число после нажатия десятичной клавиши, это число также должно появиться на дисплее.

Чтобы создать этот эффект, мы можем объединить . с отображаемым числом.

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

Далее, допустим, Мэри продолжает свои вычисления, нажимая клавишу оператора.

Когда пользователь нажимает клавишу оператора

Если Мэри нажимает клавишу оператора, оператор должен быть выделен, чтобы Мэри знала, что оператор активен.

Для этого мы можем добавить класс is-depressed к клавише оператора.

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

Как только Мэри нажмет клавишу оператора, она нажмет еще одну цифровую клавишу.

Когда пользователь нажимает цифровую клавишу после клавиши оператора

Когда Мэри снова нажимает цифровую клавишу, предыдущий дисплей должен быть заменен новым номером. Клавиша оператора также должна выйти из своего нажатого состояния.

Чтобы освободить нажатое состояние, мы удаляем класс is-depressed со всех клавиш через цикл forEach:

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

Если previousKeyType является оператором, мы хотим заменить отображаемое число на число, по которому щелкнули.

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

Далее, допустим, Мэри решает завершить вычисление нажатием клавиши равенства.

Когда пользователь нажимает клавишу равенства

Когда Мэри нажимает клавишу равенства, калькулятор должен вычислить результат, который зависит от трех значений:

  1. первое число, введенное в калькулятор
  2. Оператор
  3. второе число, введенное в калькулятор.

После расчета результат должен заменить отображаемое значение.

На данный момент нам известно только второе число, то есть текущее отображаемое число.

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
  display.textContent = calculate(firstValue, operator, secondValue)
}

Это означает, что нам нужно создать 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
  }
  return result
}

Помните, что firstValue и secondValue на этом этапе являются строками. Если вы сложите строки вместе, вы объедините их (1 + 1 = 11).

Итак, перед вычислением результата мы хотим преобразовать строки в числа. Мы можем сделать это с помощью двух функций parseInt и parseFloat.

  • parseInt преобразует строку в целое число.
  • parseFloat преобразует строку в число с плавающей запятой (это означает число с десятичными знаками).

Для калькулятора нам понадобится поплавок.

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
}

Вот и все для счастливого пути!

Вы можете получить исходный код для счастливого пути через эту ссылку (прокрутите вниз и введите свой адрес электронной почты в поле, и я отправлю исходные коды прямо на ваш почтовый ящик).

Крайние случаи

Счастливого пути недостаточно. Чтобы создать надежный калькулятор, вам нужно сделать его устойчивым к странным шаблонам ввода. Для этого вы должны представить себе нарушителя спокойствия, который пытается сломать ваш калькулятор, нажимая клавиши в неправильном порядке. Назовем этого возмутителя спокойствия Тимом.

Тим может нажимать эти клавиши в любом порядке:

  1. Цифровая клавиша (0–9)
  2. Клавиша оператора (+, -, ×, ÷)
  3. Десятичный ключ
  4. Ключ равенства
  5. Ясный ключ

Что произойдет, если Тим нажмет десятичную клавишу

Если Тим нажимает десятичную клавишу, когда на дисплее уже отображается десятичная точка, ничего не должно происходить.

Здесь мы можем проверить, что отображаемое число содержит . с помощью метода 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 + '.'
}

Затем, если Тим нажимает десятичную клавишу после нажатия клавиши оператора, на дисплее должно отображаться 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'
}
if (action === 'calculate') {
 // ...
  calculator.dataset.previousKeyType = 'calculate'
}

Получив правильный previousKeyType, мы можем использовать его, чтобы проверить, является ли предыдущий ключ оператором.

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

Что произойдет, если Тим нажмет клавишу оператора

Если Тим сначала нажимает клавишу оператора, клавиша оператора должна загореться. (Мы уже рассмотрели этот крайний случай, но как? Посмотрим, сможете ли вы определить, что мы сделали).

Во-вторых, ничего не должно произойти, если Тим несколько раз нажмет одну и ту же клавишу оператора. (Мы уже рассмотрели этот крайний случай).

Примечание. если вы хотите улучшить UX, вы можете показать, что оператор постоянно нажимается, с некоторыми изменениями CSS. Мы не делали этого здесь, но посмотрим, сможете ли вы запрограммировать это самостоятельно в качестве дополнительной задачи кодирования.

В-третьих, если Тим нажимает клавишу другого оператора после нажатия клавиши первого оператора, первая клавиша оператора должна быть отпущена. Затем следует нажать вторую клавишу оператора. (Мы рассмотрели и этот крайний случай - но как?).

В-четвертых, если Тим нажимает число, оператор, число и другой оператор в указанном порядке, на дисплее должно появиться вычисленное значение.

Это означает, что нам нужно использовать функцию calculate, когда существуют firstValue, operator и secondValue.

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
}

Хотя мы можем вычислить значение при повторном нажатии клавиши оператора, мы также ввели ошибку на этом этапе - при дополнительном нажатии клавиши оператора вычисляется значение, хотя этого не должно быть.

Чтобы калькулятор не выполнял вычисления при последующих нажатиях клавиши оператора, нам нужно проверить, является ли previousKeyType оператором. Если это так, мы не выполняем расчет.

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

В-пятых, после того, как клавиша оператора вычисляет число, если Тим нажимает на число, за которым следует другой оператор, оператор должен продолжить вычисление, например: 8 - 1 = 7, 7 - 2 = 5, 5 - 3 = 2.

В настоящий момент наш калькулятор не может производить последовательные вычисления. Второе рассчитанное значение неверно. Вот что у нас есть: 99 - 1 = 98, 98 - 1 = 0.

Второе значение вычислено неправильно, потому что мы загрузили неправильные значения в функцию calculate. Давайте рассмотрим несколько изображений, чтобы понять, что делает наш код.

Понимание нашей функции вычисления

Во-первых, предположим, что пользователь нажимает на число 99. На данный момент в калькуляторе еще ничего не зарегистрировано.

Во-вторых, допустим, пользователь нажимает оператор вычитания. После того, как они щелкают по оператору вычитания, мы устанавливаем firstValue на 99. Мы также устанавливаем operator на вычитание.

В-третьих, допустим, пользователь нажимает второе значение - на этот раз это 1. На этом этапе отображаемое число обновляется до 1, но наши firstValue, operator и secondValue остаются неизменными.

В-четвертых, пользователь снова нажимает кнопку вычитания. Сразу после того, как они нажали кнопку «Вычесть», перед вычислением результата мы устанавливаем secondValue в качестве отображаемого числа.

В-пятых, мы выполняем расчет с firstValue 99, operator вычитанием и secondValue 1. Результат - 98.

Как только результат вычислен, мы устанавливаем отображение на результат. Затем мы устанавливаем operator на вычитание и 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
}
key.classList.add('is-depressed')
calculator.dataset.previousKeyType = 'operator'
calculator.dataset.operator = action

С этим исправлением последовательные вычисления, выполняемые клавишами оператора, теперь должны быть правильными.

Что произойдет, если Тим нажмет клавишу равенства?

Во-первых, ничего не должно произойти, если Тим нажмет клавишу равенства перед любой клавишей оператора.

Мы знаем, что клавиши оператора еще не нажимались, если для 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'
}

Во-вторых, если Тим набирает число, за которым следует оператор, за которым следует равенство, калькулятор должен вычислить результат таким образом, чтобы:

  1. 2 + = —> 2 + 2 = 4
  2. 2 - = —> 2 - 2 = 0
  3. 2 × = —> 2 × 2 = 4
  4. 2 ÷ = —> 2 ÷ 2 = 1

Мы уже приняли во внимание этот странный ввод. Вы понимаете почему? :)

В-третьих, если Тим нажимает клавишу «равно» после завершения вычислений, необходимо выполнить еще одно вычисление. Вот как должен выглядеть расчет:

  1. Тим нажимает клавиши 5–1
  2. Тим бьет с равным. Расчетное значение 5 - 1 = 4
  3. Тим бьет с равным. Расчетное значение 4 - 1 = 3
  4. Тим бьет с равным. Расчетное значение 3 - 1 = 2
  5. Тим бьет с равным. Расчетное значение 2 - 1 = 1
  6. Тим бьет с равным. Расчетное значение 1 - 1 = 0

К сожалению, наш калькулятор не дает точных расчетов. Вот что показывает наш калькулятор:

  1. Тим нажимает клавишу 5–1
  2. Тим бьет с равным. Расчетное значение 4
  3. Тим бьет с равным. Расчетное значение 1

Исправление расчета

Во-первых, предположим, что наш пользователь нажимает 5. На данный момент в калькуляторе еще ничего не зарегистрировано.

Во-вторых, допустим, пользователь нажимает оператор вычитания. После того, как они щелкают по оператору вычитания, мы устанавливаем firstValue на 5. Мы также устанавливаем operator на вычитание.

В-третьих, пользователь нажимает на второе значение. Допустим, это 1. На этом этапе отображаемое число обновляется до 1, но наши firstValue, operator и secondValue остаются без изменений.

В-четвертых, пользователь нажимает клавишу равенства. Сразу после нажатия кнопки равно, но перед вычислением мы устанавливаем secondValue как displayedNum.

В-пятых, калькулятор вычисляет результат 5 - 1 и дает 4. Результат обновляется на дисплее. firstValue и operator переносятся на следующий расчет, поскольку мы не обновляли их.

В-шестых, когда пользователь снова нажимает "равно", мы устанавливаем secondValue на displayedNum перед вычислением.

Вы можете сказать, что здесь не так.

Вместо 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'
}

Мы также хотим перенести предыдущий 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'
}

Если previousKeyType равно calculate, мы знаем, что можем использовать calculator.dataset.modValue как secondValue. Как только мы это узнаем, мы сможем выполнить расчет.

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

При этом у нас есть правильный расчет при последовательном нажатии клавиши равенства.

Вернуться к ключу равенства

В-четвертых, если Тим нажимает десятичную клавишу или цифровую клавишу после клавиши калькулятора, дисплей должен быть заменен на 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.'
  }
calculator.dataset.previousKeyType = 'decimal'
}

В-пятых, если Тим нажимает клавишу оператора сразу после клавиши равенства, калькулятор не вычисляет.

Для этого мы проверяем, является ли 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
  }
// ...
}

Ключ очистки имеет два применения:

  1. All Clear (обозначается AC) очищает все и сбрасывает калькулятор в исходное состояние.
  2. Очистить запись (обозначается CE) очищает текущую запись. Он сохраняет в памяти предыдущие числа.

Когда калькулятор находится в состоянии по умолчанию, должно отображаться AC.

Во-первых, если Тим нажимает клавишу (любую клавишу, кроме Clear), AC следует изменить на CE.

Мы делаем это, проверяя, является ли data-action clear. Если это не clear, ищем кнопку очистки и меняем ее textContent.

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

Во-вторых, если Тим нажимает CE, на дисплее должно отображаться 0. В то же время CE следует вернуться к AC, чтобы Тим мог сбросить калькулятор в исходное состояние. **

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

В-третьих, если Тим ударит AC, сбросьте калькулятор в исходное состояние.

Чтобы вернуть калькулятор в исходное состояние, нам нужно очистить все установленные нами настраиваемые атрибуты.

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

Вот и все - по крайней мере, для части крайних случаев!

Вы можете получить исходный код для части крайних случаев через эту ссылку (прокрутите вниз и введите свой адрес электронной почты в поле, и я отправлю исходные коды прямо на ваш почтовый ящик).

На данный момент код, который мы создали вместе, довольно запутан. Вы, вероятно, заблудитесь, если попытаетесь прочитать код самостоятельно. Давайте проведем рефакторинг, чтобы он стал чище.

Рефакторинг кода

Когда вы проводите рефакторинг, вы часто начинаете с наиболее очевидных улучшений. В этом случае давайте начнем с calculate.

Прежде чем продолжить, убедитесь, что вы знаете эти методы / функции JavaScript. Мы будем использовать их в рефакторинге.

  1. Раннее возвращение
  2. Тернарные операторы
  3. Чистые функции
  4. Разрушение ES6

Итак, приступим!

Рефакторинг функции вычисления

Вот что у нас есть на данный момент.

const calculate = (n1, operator, n2) => {
  let result = ''
  if (operator === 'add') {
    result = firstNum + parseFloat(n2)
  } else if (operator === 'subtract') {
    result = parseFloat(n1) - parseFloat(n2)
  } else if (operator === 'multiply') {
    result = parseFloat(n1) * parseFloat(n2)
  } else if (operator === 'divide') {
    result = parseFloat(n1) / parseFloat(n2)
  }
  return result
}

Вы узнали, что нам следует максимально сократить количество перераспределений. Здесь мы можем удалить присвоения, если вернем результат вычислений в операторах 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 (operator === 'divide') {
    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

Как упоминалось ранее, 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!) {
    return displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
      ? keyContent
      : displayedNum + keyContent
  }
}

Создание строки результата для десятичного ключа

Вот код десятичного ключа:

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

Как и раньше, мы хотим переместить все, что изменяет display.textContent на createResultString.

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

Поскольку мы хотим вернуть все значения, мы можем преобразовать else if операторы в ранние возвраты.

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

Распространенная ошибка здесь - забыть вернуть отображаемое в настоящее время число, если ни одно из условий не соответствует. Нам это нужно, потому что мы заменим display.textContent значением, возвращенным из createResultString. Если мы его пропустили, createResultString вернет undefined, чего мы не желаем.

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

Как всегда, обратите внимание на необходимые переменные. На этом этапе требуемые переменные остаются такими же, как и раньше:

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

Вы уже знаете, что такое упражнение: мы хотим переместить все, что изменяет 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 переменную. Мы можем использовать displayedNum непосредственно в функции calculate.

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

Как и выше, хочу переместить все, что меняет 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'
}

Как и выше, мы хотим скопировать все, что изменяет 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)
  }
}

При копировании кода в 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
}

Передача необходимых переменных

Нам нужно семь свойств / переменных в createResultString:

  1. keyContent
  2. displayedNum
  3. previousKeyType
  4. action
  5. firstValue
  6. modValue
  7. operator

Мы можем получить keyContent и action от key. Мы также можем получить firstValue, modValue, operator и previousKeyType от calculator.dataset.

Это означает, что функции 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
  // ...
}

Согласованность внутри операторов if

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

Они непостоянны, поэтому их трудно читать. Если возможно, мы хотим сделать их согласованными, чтобы мы могли написать что-то вроде этого:

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)
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

Мы закончили с createResultString. Перейдем к updateCalculatorState.

Делаем updateCalculatorState

updateCalculatorState - это функция, которая изменяет внешний вид калькулятора и настраиваемые атрибуты.

Как и в случае с createResultString, нам нужно проверить тип нажатой клавиши. Здесь мы можем повторно использовать getKeyType.

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

Если вы посмотрите на оставшийся код, вы можете заметить, что мы меняем 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'
  }
}

Это избыточно, потому что мы уже знаем тип ключа с 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 для клавиш оператора

Визуально нам нужно убедиться, что все клавиши выходят из своего нажатого состояния. Здесь мы можем скопировать и вставить код, который у нас был раньше:

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

Вот что осталось от того, что мы написали для клавиш оператора после перемещения частей, связанных с 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
}

Вы можете заметить, что мы можем сократить код с помощью тернарного оператора:

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'
  }
}
if (action !== 'clear') {
  const clearButton = calculator.querySelector('[data-action=clear]')
  clearButton.textContent = 'CE'
}

Здесь нет ничего особенного, что можно было бы реорганизовать. Не стесняйтесь копировать / вставлять все в 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
    }
  }
  calculator.dataset.modValue = secondValue
}

Мы можем преобразовать это в следующее:

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 нам нужно пять переменных / свойств:

  1. key
  2. calculator
  3. calculatedValue
  4. displayedNum
  5. 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
  // Pass in necessary values
  updateCalculatorState(key, calculator, resultString, displayedNum)
})

Повторный рефакторинг updateCalculatorState

В updateCalculatorState мы изменили три вида значений:

  1. calculator.dataset
  2. Класс для нажатия / депрессии операторов
  3. 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'
  }
  if (keyType !== 'clear') {
    const clearButton = calculator.querySelector('[data-action=clear]')
    clearButton.textContent = 'CE'
  }
}

Подведение итогов

После рефакторинга код стал намного чище. Если вы посмотрите в прослушиватель событий, вы узнаете, что делает каждая функция. Вот как выглядит прослушиватель событий в конце:

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

Вы можете получить исходный код для части рефакторинга через эту ссылку (прокрутите вниз и введите свой адрес электронной почты в поле, и я отправлю исходные коды прямо на ваш почтовый ящик).

Надеюсь, вам понравилась эта статья. Если да, то вам, возможно, понравится Learn JavaScript - курс, в котором я покажу вам, как построить 20 компонентов, шаг за шагом, подобно тому, как мы построили этот калькулятор сегодня.

Примечание: мы можем улучшить калькулятор, добавив поддержку клавиатуры и специальные возможности, такие как живые регионы. Хотите узнать как? Пойдите, проверьте Learn JavaScript :)