如何使用 JavaScript 构建计算器应用程序

这个史诗般的教程通过描述如何使用该语言开发一个简单的计算器应用程序,为 JavaScript 新手提供了一个可靠的练习。

面向初学者的 javascript 项目(8 部分系列)

我们将在本教程中开发的计算器应用程序非常简单。它或多或少与杂货店中的那些计算器类似。应用程序的布局是用CSS 网格制作的。如果您想了解它是如何实现的,请务必查看本教程,因为本教程仅关注用于使计算器工作的 JavaScript 逻辑。

JavaScript 计算器演示

这是已完成项目的现场演示。在潜入之前,您可以使用它来感受一下您将要构建的内容。

先决条件

本教程假设您具有 JavaScript 的基本知识。我将尽可能分解每个步骤,因此即使您之前没有在浏览器中构建应用程序的经验,也应该很容易理解。

在你开始之前

你可以找到起点 本教程上的jsfiddle。它包含用于构建计算器布局的所有必要标记和样式。标记几乎与上一教程的最终状态相同,在该教程中我讨论了如何制作计算器布局,但我做了一些小改动,因此请务必使用这个。

首先将代码分叉到一个新的小提琴上,然后输入每一步直到最后。如果您愿意,可以在其他在线代码游乐场或本地机器上随意完成本教程。

入门

通过使用输入按钮构建有效表达式,任何人都应该能够在我们的计算器应用程序上执行四种最常见的算术运算(加法、减法、乘法和除法),并将结果显示在屏幕上。有效表达式的示例如下所示:

12 + 10

要构造一个有效的算术表达式,我们需要跟踪一些事情:第一个操作数 ( 12)、运算符 ( +) 和第二个操作数 ( 10)。

让我们首先创建一个对象来帮助我们跟踪这些值。在 JSFiddle 的 JavaScript 窗格顶部添加以下代码:

const calculator = {
  displayValue: '0',
  firstOperand: null,
  waitingForSecondOperand: false,
  operator: null,
};

calculator上面的对象包含我们构建有效表达式所需的一切:

  • displayValue保存表示用户输入或操作结果的字符串值。这就是我们如何跟踪应该在屏幕上显示的内容。
  • firstOperand将存储任何表达式的第一个操作数。暂时就这样 null了。
  • operator键将存储在运营商的表达式。它的初始值也是null
  • waitingForSecondOperand本质上用作检查第一个操作数和运算符是否都已输入的方法。如果是true,则用户输入的下一个数字将构成第二个操作数。

更新显示

目前,计算器屏幕是空白的。我们需要displayValue始终在屏幕上显示属性的内容 。我们将为此创建一个函数,以便任何时候在应用程序中执行操作时,我们都可以调用它以使用 displayValue.

继续并在calculator对象下方键入以下内容:

function updateDisplay() {
  // select the element with class of `calculator-screen`
  const display = document.querySelector('.calculator-screen');
  // update the value of the element with the contents of `displayValue`
  display.value = calculator.displayValue;
}

updateDisplay();

如果您查看应用程序的HTML代码,您会发现“屏幕”实际上只是一个禁用的文本输入:

<input type="text" class="calculator-screen" value="" disabled />

我们不能直接用键盘输入,但我们可以用 JavaScript 改变它的值。这就是updateDisplay函数所做的。现在您应该看到计算器屏幕上显示零。

喘口气, 在这一步结束时查看 完整的代码

处理按键

计算器上有四组键:数字(0-9)、运算符(+、-、⨉、÷、=)、小数点(.)和复位键(AC)。在本节中,我们将监听对计算器键的点击,并确定点击的是什么类型的键。

在 JavaScript 选项卡底部添加此代码片段:

const keys = document.querySelector('.calculator-keys');
keys.addEventListener('click', (event) => {
  // Access the clicked element
  const { target } = event;

  // Check if the clicked element is a button.
  // If not, exit from the function
  if (!target.matches('button')) {
    return;
  }

  if (target.classList.contains('operator')) {
    console.log('operator', target.value);
    return;
  }

  if (target.classList.contains('decimal')) {
    console.log('decimal', target.value);
    return;
  }

  if (target.classList.contains('all-clear')) {
    console.log('clear', target.value);
    return;
  }

  console.log('digit', target.value);
});

在上面的代码片段中,我们正在侦听类为click的元素上的事件calculator-keys。由于计算器上的所有键都是该元素的子元素,因此click事件也会过滤到它们。这称为 事件委托

在事件侦听器的回调函数中,我们target 使用解构赋值 来提取单击事件的属性,这使得将对象属性解包为不同的变量变得容易。

const { target } = event;
// is equivalent to
const target = event.target;

target变量是表示被点击的元件的对象。如果此元素不是按钮(例如,如果您单击按钮之间的空格),我们将通过提前返回来退出该功能。否则,单击的按钮类型及其值将记录到控制台。

在继续下一步之前,请务必尝试一下。打开浏览器控制台并单击任何按钮。键的类型和值应相应地记录到控制台。

检测到正确类型的密钥并将其记录到控制台

检测到正确类型的密钥并将其记录到控制台

喘口气, 在这一步结束时查看 完整的代码

输入数字

在这一步中,我们将使数字按钮起作用,以便在单击它们中的任何一个时,屏幕上都会显示相应的值。

单击数字键时向用户显示反馈

由于对象的displayValue属性calculator代表用户的输入,所以我们需要在点击任何数字时对其进行修改。创建一个inputDigitcalculator对象下面调用的新函数:

function inputDigit(digit) {
  const { displayValue } = calculator;
  // Overwrite `displayValue` if the current value is '0' otherwise append to it
  calculator.displayValue = displayValue === '0' ? digit : displayValue + digit;
}

接下来,替换click事件侦听器回调函数中的以下行:

console.log('digit', target.value);

使用以下代码:

inputDigit(target.value);
updateDisplay();

inputDigit函数中,三元运算符 ( ?) 用于检查计算器上显示的当前值是否为零。如果是,calculator.displayValue则被单击的任何数字覆盖。否则,如果displayValue是非零数字,则通过字符串连接将数字附加到其上。

最后,updateDisplay()调用该函数,以便在displayValue单击每个按钮后将属性的新内容反映在屏幕上。通过单击任何数字按钮进行尝试。显示应更新为您单击的任何数字。

喘口气, 在这一步结束时查看 完整的代码

输入小数点

当点击小数点键时,我们需要在屏幕上显示的任何内容后附加一个小数点,除非它已经包含小数点。

计算器显示输入的小数点

这是我们如何实现这一目标。创建一个名为inputDecimal 下面的新函数inputDigit

function inputDecimal(dot) {
  // If the `displayValue` property does not contain a decimal point
  if (!calculator.displayValue.includes(dot)) {
    // Append the decimal point
    calculator.displayValue += dot;
  }
}

在该inputDecimal函数中, includes() 方法用于检查是否displayValue尚未包含小数点。如果是,则会在数字后附加一个点。否则,函数退出。

之后,替换键的事件侦听器回调函数中的以下行:

console.log('decimal', target.value);

使用以下代码行:

inputDecimal(target.value);
updateDisplay();

此时,您应该可以成功添加输入小数点并显示在屏幕上。

喘口气, 在这一步结束时查看 完整的代码

处理操作员

下一步是让计算器上的运算符(+、-、⨉、÷、=)工作。需要考虑三种情况:

1.当用户输入第一个操作数后点击操作符时

此时, 的内容displayValue应该存储在firstOperand属性下, 并且operator应该使用单击的任何运算符更新属性。

创建一个名为handleOperator下面的新函数inputDecimal

function handleOperator(nextOperator) {
  // Destructure the properties on the calculator object
  const { firstOperand, displayValue, operator } = calculator
  // `parseFloat` converts the string contents of `displayValue`
  // to a floating-point number
  const inputValue = parseFloat(displayValue);

  // verify that `firstOperand` is null and that the `inputValue`
  // is not a `NaN` value
  if (firstOperand === null && !isNaN(inputValue)) {
    // Update the firstOperand property
    calculator.firstOperand = inputValue;
  }

  calculator.waitingForSecondOperand = true;
  calculator.operator = nextOperator;
}

当按下操作符键时, 的内容displayValue被转换为浮点数(即带小数点的数字)并将结果存储在firstOperand属性中。

operator属性还设置为单击任何操作符键,而 waitingForSecondOperand设置为true表示已输入第一个操作数,用户接下来输入的任何数字将构成第二个操作数。

此时,查看calculator每次按下按钮时对象的属性如何更新是很有用的。将以下行添加到inputDigithandleOperator函数的末尾:

console.log(calculator);

然后在键的click事件侦听器回调函数中替换以下行:

console.log('operator', target.value);

使用以下代码:

handleOperator(target.value);
updateDisplay();

此时,依次单击以下键,尝试构造有效的算术运算:12 +。请注意,当+按下该键时,firstOperandoperator属性的值分别更新为12和 ,+ 而waitingForSecondOperand设置为 true 表示计算器现在正在等待输入第二个操作数。

显示计算器对象的控制台

如果您尝试输入第二个操作数,您会注意到它被附加到第一个操作数而不是覆盖它。

计算器显示如何将第二个操作数附加到第一个

让我们通过更新inputDigit函数来解决这个问题,如下所示:

function inputDigit(digit) {
  const { displayValue, waitingForSecondOperand } = calculator;

  if (waitingForSecondOperand === true) {
    calculator.displayValue = digit;
    calculator.waitingForSecondOperand = false;
  } else {
    calculator.displayValue = displayValue === '0' ? digit : displayValue + digit;
  }

  console.log(calculator);
}

如果该waitingForSecondOperand属性设置为true,则该displayValue 属性将被单击的数字覆盖。否则,将执行与之前相同的检查,displayValue酌情覆盖或附加。

喘口气, 在这一步结束时查看 完整的代码

2.当用户输入第二个操作数后点击操作符时

我们需要处理的第二种情况发生在用户输入第二个操作数并单击操作键时。此时,评估表达式的所有成分都存在,因此我们需要这样做并将结果显示在屏幕上。该firstOperand也需要更新,以便结果能在接下来的计算中重复使用。

创建一个calculate如下handleOperator所示的新函数:

function calculate(firstOperand, secondOperand, operator) {
  if (operator === '+') {
    return firstOperand + secondOperand;
  } else if (operator === '-') {
    return firstOperand - secondOperand;
  } else if (operator === '*') {
    return firstOperand * secondOperand;
  } else if (operator === '/') {
    return firstOperand / secondOperand;
  }

  return secondOperand;
}

此函数将第一个操作数、第二个操作数和运算符作为参数,并检查运算符的值以确定应如何计算表达式。如果运算符是=,则第二个操作数将按原样返回。

接下来,更新handleOperator函数,如下所示:

function handleOperator(nextOperator) {
  const { firstOperand, displayValue, operator } = calculator
  const inputValue = parseFloat(displayValue);

  if (firstOperand == null && !isNaN(inputValue)) {
    calculator.firstOperand = inputValue;

  } else if (operator) {

    const result = calculate(firstOperand, inputValue, operator);



    calculator.displayValue = String(result);

    calculator.firstOperand = result;

  }


  calculator.waitingForSecondOperand = true;
  calculator.operator = nextOperator;
  console.log(calculator);
}

else if添加的块handleOperator检查operator属性是否已分配给运算符。如果是,calculate则调用该函数并将结果保存在result变量中。

该结果随后通过更新displayValue 属性显示给用户。此外, 的值firstOperand被更新为结果,以便它可以在下一个计算器中使用。

试试看。输入12 + 10 =计算器并注意屏幕上显示正确的结果。当您链接一系列操作时,它也适用。所以5 * 20 - 14 =应该给86作为结果。

计算器显示 5 * 20 - 14 的结果

这是因为打减号键触发第一动作的计算(5 * 20),其结果(100)随后被设定为 firstOperand以时间为下一次计算,所以我们进入14第二操作数和命中=键,calculate执行功能再次得到86作为结果也被设置为firstOperand下一个操作,依此类推。

喘口气, 在这一步结束时查看 完整的代码

3. 连续输入两个或多个操作符时

改变一个人想要执行的操作类型的想法是很常见的,因此计算器必须正确处理它。

假设您想将 7 和 2 相加,您将单击7 + 2 =这将产生正确的结果。但是让我们假设在点击 之后7 +,您改变主意并决定从 7 中减去 2。而不是清除计算器并重新开始,您应该能够点击-覆盖 +之前输入的 。

请记住,在输入运算符时,waitingForSecondOperand 将被设置为 ,true因为计算器期望在运算符键之后输入第二个操作数。我们可以使用这种特性来更新操作符键并防止任何计算,直到输入第二个操作数。

handleOperator函数修改为如下所示:

function handleOperator(nextOperator) {
  const { firstOperand, displayValue, operator } = calculator
  const inputValue = parseFloat(displayValue);


  if (operator && calculator.waitingForSecondOperand)  {

    calculator.operator = nextOperator;

    console.log(calculator);

    return;

  }


  if (firstOperand == null && !isNaN(inputValue)) {
    calculator.firstOperand = inputValue;
  } else if (operator) {
    const result = calculate(firstOperand, inputValue, operator);
    calculator.displayValue = String(result);
    calculator.firstOperand = result;
  }

  calculator.waitingForSecondOperand = true;
  calculator.operator = nextOperator;
  console.log(calculator);
}

相关变化在上面突出显示。该if语句检查 a 是否 operator已经存在,并且 ifwaitingForSecondOperand设置为true。如果是,则operator属性的值将替换为 new 运算符并且函数退出,以便不执行任何计算。

试试看。输入一些数字后单击多个运算符并在控制台中监视计算器对象。请注意,该operator属性每次都会更新,并且在您提供第二个操作数之前不会执行任何计算。

喘口气, 在这一步结束时查看 完整的代码

重置计算器

最后的任务是确保用户可以通过按键将计算器重置为其初始状态。在大多数计算器中,该AC按钮用于将计算器重置为默认状态,这就是我们在这里要做的。

继续在下面创建一个新函数,如下calculate所示:

function resetCalculator() {
  calculator.displayValue = '0';
  calculator.firstOperand = null;
  calculator.waitingForSecondOperand = false;
  calculator.operator = null;
  console.log(calculator);
}

然后在键的事件侦听器回调函数中替换以下行:

console.log('all clear', target.value)

使用以下代码:

resetCalculator();
updateDisplay();

resetCalculator函数将calculator 对象的所有属性设置为其原始值。单击AC计算器上的键现在应该可以正常工作了。您可以calculator在控制台中查看对象进行确认。

喘口气, 在这一步结束时查看 完整的代码

修复小数错误

如果在单击运算符后输入小数点,它会附加到第一个操作数而不是第二个操作数的一部分。

计算器 gif 显示十进制错误

我们可以通过对inputDecimal 函数进行以下修改来修复此错误:

function inputDecimal(dot) {

  if (calculator.waitingForSecondOperand === true) {

  	calculator.displayValue = '0.'

    calculator.waitingForSecondOperand = false;

    return

  }


  if (!calculator.displayValue.includes(dot)) {
    calculator.displayValue += dot;
  }
}

IfwaitingForSecondOperand设置为true并输入小数点, displayValue变为0.waitingForSecondOperand设置为 false 以便任何其他数字作为第二个操作数的一部分附加。

GIF 显示已修复的错误

该错误现已修复

喘口气, 在这一步结束时查看 完整的代码

重构事件监听器

更新键的事件侦听器,如下所示。所有if块都已被替换为单个switch块,并且updateDisplay()仅在函数结束时调用一次。

keys.addEventListener('click', event => {
  const { target } = event;
  const { value } = target;
  if (!target.matches('button')) {
    return;
  }

  switch (value) {
    case '+':
    case '-':
    case '*':
    case '/':
    case '=':
      handleOperator(value);
      break;
    case '.':
      inputDecimal(value);
      break;
    case 'all-clear':
      resetCalculator();
      break;
    default:
      // check if the key is an integer
      if (Number.isInteger(parseFloat(value))) {
        inputDigit(value);
      }
  }

  updateDisplay();
});

这样,向计算器添加新函数就容易多了,而且您不再需要updateDisplay()在每次操作后调用该函数。

喘口气, 在这一步结束时查看 完整的代码

浮点精度

我想提请您注意当运算结果为浮点数时出现的问题。例如,0.1 + 0.2产生 0.30000000000000004而不是0.3您可能期望的那样。

浮点精度错误

在其他情况下,您会得到预期的结果。例如0.1 + 0.4产量0.5。可以在此处找到有关为什么会发生这种情况的详细说明 。请务必阅读它。

上述链接页面中建议的解决此问题的潜在解决方案之一是将结果格式化为固定的小数位数,以便丢弃其他小数位。我们可以将 JavaScript parseFloat 函数与 Number.toFixed方法结合起来, 在我们的计算器应用中实现这个解决方案。

handleOperator函数中,替换以下行:

calculator.displayValue = String(result);

使用以下代码:

calculator.displayValue = `${parseFloat(result.toFixed(7))}`;

toFixed()方法接受 0 到 20 之间的值,并确保小数点后的位数限制为该值。如有必要,返回值可以四舍五入或用零填充。

在前面的例子中,0.1 + 0.2yield 0.30000000000000004toFixed(7)在结果上使用 会将小数点后的数字限制为七位:

0.30000000000000004.toFixed(7) // 0.3000000

那些额外的零并不重要,所以我们可以用它parseFloat来摆脱它们:

parseFloat(0.30000000000000004.toFixed(7)) // 0.3

这就是我们如何能够在我们的应用程序中解决这个问题。在这种情况下,我选择了数字 7,因为它对于这个计算器应用程序来说已经足够好了。在其他情况下可能需要更高的精度。

显示固定浮点精度的 GIF

作为额外的奖励,结果现在适合屏幕

喘口气, 在这一步结束时查看 完整的代码

奖金内容

我已经为我的 Patreon 支持者准备了一些对这个计算器应用程序的进一步增强。应用程序中添加了以下函数:sin、cos、tan、log、square、square root、factorial、percent、plus/minus、pi 和 ce。

JavaScript 计算器奖励

不要忘记在GitHub - Freshman-tech/javascript-calculator: Build a simple calculator app with JavaScriptBuild a simple calculator app with JavaScript. Contribute to Freshman-tech/javascript-calculator development by creating an account on GitHub.https://github.com/Freshman-tech/javascript-calculator查看完整的源代码。

结论

我的教程到此结束,希望你能从中学到很多。如果文章的某个部分对您来说不够清楚,请随时发表评论,并 订阅我的时事通讯以在您的收件箱中获得更多像这样的精彩教程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值