You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript科学计算器三角函数功能失效问题(分词替代eval)

问题描述

我正在用JavaScript开发一款科学计算器,基础计算功能正常,但三角函数等科学计算功能无法工作。之前用eval()实现计算逻辑,出于安全考虑改成了分词(tokenization)方法,现在三角函数计算失效。

计算器三角函数失效截图

完整代码

JavaScript部分

const userInput = document.getElementById('input');
const userOutput = document.getElementById('output');
const clearbtn = document.getElementById('clear');
const calBtn = document.getElementById('calcBtn');
const btns = document.querySelectorAll('input[type="button"]');

function calc() {
  btns.forEach(button => {
    button.addEventListener('click', () => {
      const btnDataType = button.dataset.type;

      if (btnDataType === 'number' || btnDataType === 'operator') {
        userInput.value += button.value;
      } else if (btnDataType === 'Soperator') {
        userInput.value += `${button.value}(`;
      }

      calBtn.addEventListener("click", () => {
        let tokens = userInput.value.match(/(\d+(\.\d+)?)|[+\-*/%()]|sin|cos|tan/g);
        if (!tokens) {
          console.log("Error: Invalid input");
          return;
        }

        console.log("Tokens:", tokens);

        for (let i = 0; i < tokens.length; i++) {
          if (['sin', 'cos', 'tan'].includes(tokens[i]) && tokens[i + 1] === '(') {
            let argStart = i + 2;
            let argEnd = tokens.indexOf(')', argStart);
            if (argEnd === -1) {
              console.log("Error: Missing closing parenthesis");
              return;
            }

            let angle = parseFloat(tokens.slice(argStart, argEnd).join(''));
            let result;

            if (tokens[i] === 'sin') {
              result = Math.sin(angle * (Math.PI / 180));
            } else if (tokens[i] === 'cos') {
              result = Math.cos(angle * (Math.PI / 180));
            } else if (tokens[i] === 'tan') {
              result = Math.tan(angle * (Math.PI / 180));
            }

            tokens.splice(i, argEnd - i + 1, result.toString());
            i--;
          }
        }

        for (let i = 0; i < tokens.length; i++) {
          if (['*', '/', '%'].includes(tokens[i])) {
            let left = parseFloat(tokens[i - 1]);
            let right = parseFloat(tokens[i + 1]);
            let result = tokens[i] === '*' ? left * right : tokens[i] === '/' ? left / right : left % right;
            tokens.splice(i - 1, 3, result.toString());
            i--;
          }
        }

        for (let i = 0; i < tokens.length; i++) {
          if (['+', '-'].includes(tokens[i])) {
            let left = parseFloat(tokens[i - 1]);
            let right = parseFloat(tokens[i + 1]);
            let result = tokens[i] === '+' ? left + right : left - right;
            tokens.splice(i - 1, 3, result.toString());
            i--;
          }
        }

        userOutput.value = tokens[0];
      });
    });
  });

  document.getElementById("cut").addEventListener('click', () => {
    userInput.value = userInput.value.slice(0, -1);
  });

  clearbtn.addEventListener('click', () => {
    userInput.value = '';
    userOutput.value = '';
  });
}

calc();

HTML部分

<!-- header section -->
<header>
  <nav>
    <a href="">Logo</a>
    <button>contribute here</button>
  </nav>
</header>

<!-- main -->
<main>
  <section>
    <!-- calculator wrapper div -->
    <div>
      <!-- input and output screens -->
      <div>
        <input type="text" placeholder="Type to Calculate" id="input">
        <input type="text" placeholder="Output" readonly id="output">
        <button id="clear">Clear</button>
      </div>

      <!-- wrapper that contains number btns , operators -->
      <div>

        <!-- numbers -->
        <div>
          <input type="button" value="0" id="0" data-type="number">
          <input type="button" value="1" id="1" data-type="number">
          <input type="button" value="2" id="2" data-type="number">
          <input type="button" value="3" id="3" data-type="number">
          <input type="button" value="4" id="4" data-type="number">
          <input type="button" value="5" id="5" data-type="number">
          <input type="button" value="6" id="6" data-type="number">
          <input type="button" value="7" id="7" data-type="number">
          <input type="button" value="8" id="8" data-type="number">
          <input type="button" value="9" id="9" data-type="number">
        </div>

        <!-- operators btns -->

        <div>
          <input type="button" value="+" id="+" data-type="operator">
          <input type="button" value="-" id="-" data-type="operator">
          <input type="button" value="*" id="*" data-type="operator">
          <input type="button" value="/" id="/" data-type="operator">
          <input type="button" value="%" id="%" data-type="operator">
          <button id="calcBtn">Calculate</button>
          <button id="cut">X</button>
        </div>

      </div>
    </div>


    <!-- scientific calculator -->

    <div>
      <input type="button" value="Sin" id="sin" data-type="Soperator">
      <input type="button" value="Cos" id="cos" data-type="Soperator">
      <input type="button" value="Tan" id="tan" data-type="Soperator">
      <input type="button" value="sin⁻¹" id="sin-in" data-type="Soperator">
      <input type="button" value="cos⁻¹" id="cos-in" data-type="Soperator">
      <input type="button" value="tan⁻¹" id="tan-in" data-type="Soperator">
    </div>
  </section>
  <section>
  </section>
</main>
<script src="cal.js"></script>
修复方案

问题根源

  1. 大小写不匹配:分词正则匹配的是小写sin|cos|tan,但按钮输入的是大写开头的Sin(、Cos(、Tan(,导致分词时无法识别三角函数关键字,处理逻辑完全没触发。
  2. 事件重复绑定:计算按钮的点击事件被嵌套在所有按钮的点击监听里,每次点击任意按钮都会给计算按钮新增一次事件绑定,导致多次执行计算逻辑。

修改后的完整代码

JavaScript部分

const userInput = document.getElementById('input');
const userOutput = document.getElementById('output');
const clearbtn = document.getElementById('clear');
const calBtn = document.getElementById('calcBtn');
const btns = document.querySelectorAll('input[type="button"]');

function calc() {
  btns.forEach(button => {
    button.addEventListener('click', () => {
      const btnDataType = button.dataset.type;

      if (btnDataType === 'number' || btnDataType === 'operator') {
        userInput.value += button.value;
      } else if (btnDataType === 'Soperator') {
        // 统一转成小写格式,同时把反函数符号转成易识别的格式
        const funcName = button.value.toLowerCase().replace('⁻¹', '-1');
        userInput.value += `${funcName}(`;
      }
    });
  });

  // 将计算按钮事件移到外层,避免重复绑定
  calBtn.addEventListener("click", () => {
    // 扩展正则,支持三角函数及反函数
    let tokens = userInput.value.match(/(\d+(\.\d+)?)|[+\-*/%()]|sin|cos|tan|sin-1|cos-1|tan-1/g);
    if (!tokens) {
      userOutput.value = "输入无效";
      return;
    }

    // 先处理三角函数(包括反函数)
    for (let i = 0; i < tokens.length; i++) {
      const func = tokens[i];
      if ((['sin', 'cos', 'tan', 'sin-1', 'cos-1', 'tan-1'].includes(func)) && tokens[i + 1] === '(') {
        let argStart = i + 2;
        let argEnd = tokens.indexOf(')', argStart);
        if (argEnd === -1) {
          userOutput.value = "缺少闭合括号";
          return;
        }

        // 先计算括号内的表达式(支持运算,比如sin(30+20))
        let argTokens = tokens.slice(argStart, argEnd);
        // 处理乘除模
        for (let j = 0; j < argTokens.length; j++) {
          if (['*', '/', '%'].includes(argTokens[j])) {
            let left = parseFloat(argTokens[j - 1]);
            let right = parseFloat(argTokens[j + 1]);
            let result = argTokens[j] === '*' ? left * right : argTokens[j] === '/' ? left / right : left % right;
            argTokens.splice(j - 1, 3, result.toString());
            j--;
          }
        }
        // 处理加减
        for (let j = 0; j < argTokens.length; j++) {
          if (['+', '-'].includes(argTokens[j])) {
            let left = parseFloat(argTokens[j - 1]);
            let right = parseFloat(argTokens[j + 1]);
            let result = argTokens[j] === '+' ? left + right : left - right;
            argTokens.splice(j - 1, 3, result.toString());
            j--;
          }
        }

        let angle = parseFloat(argTokens[0]);
        let result;

        switch(func) {
          case 'sin':
            result = Math.sin(angle * (Math.PI / 180));
            break;
          case 'cos':
            result = Math.cos(angle * (Math.PI / 180));
            break;
          case 'tan':
            result = Math.tan(angle * (Math.PI / 180));
            break;
          case 'sin-1':
            result = Math.asin(angle) * (180 / Math.PI);
            break;
          case 'cos-1':
            result = Math.acos(angle) * (180 / Math.PI);
            break;
          case 'tan-1':
            result = Math.atan(angle) * (180 / Math.PI);
            break;
        }

        tokens.splice(i, argEnd - i + 1, result.toString());
        i--;
      }
    }

    // 处理乘除模
    for (let i = 0; i < tokens.length; i++) {
      if (['*', '/', '%'].includes(tokens[i])) {
        let left = parseFloat(tokens[i - 1]);
        let right = parseFloat(tokens[i + 1]);
        let result = tokens[i] === '*' ? left * right : tokens[i] === '/' ? left / right : left % right;
        tokens.splice(i - 1, 3, result.toString());
        i--;
      }
    }

    // 处理加减
    for (let i = 0; i < tokens.length; i++) {
      if (['+', '-'].includes(tokens[i])) {
        let left = parseFloat(tokens[i - 1]);
        let right = parseFloat(tokens[i + 1]);
        let result = tokens[i] === '+' ? left + right : left - right;
        tokens.splice(i - 1, 3, result.toString());
        i--;
      }
    }

    userOutput.value = tokens[0];
  });

  document.getElementById("cut").addEventListener('click', () => {
    userInput.value = userInput.value.slice(0, -1);
  });

  clearbtn.addEventListener('click', () => {
    userInput.value = '';
    userOutput.value = '';
  });
}

calc();

关键修改说明

  • 统一格式匹配:点击科学按钮时将函数名转为小写,同时把sin⁻¹转成sin-1,确保正则能准确识别。
  • 修复事件绑定:将计算按钮的点击事件移到外层循环外,避免重复绑定导致多次执行。
  • 支持复杂参数:新增括号内表达式计算逻辑,支持sin(30+20)这类带运算的参数。
  • 扩展反三角函数:新增sin⁻¹、cos⁻¹、tan⁻¹的计算支持。

内容的提问来源于stack exchange,提问作者Adarsh tripathi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 00:19:51